Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Understanding Haskell Type Signatures

I am in the process of teaching myself Haskell and I was wondering about the following type signatures:

Prelude> :t ($) ($) :: (a -> b) -> a -> b Prelude> 

How should I interpret (no pun intended) that?

A semi-similar result is also proving to be puzzling:

Prelude> :t map map :: (a -> b) -> [a] -> [b] Prelude> 
like image 514
CaitlinG Avatar asked Mar 11 '14 21:03

CaitlinG


People also ask

Which function is used to find type signature in Haskell?

Since the type of elem y z is Bool , this matches with the function (&&) x , and thus the type of (&&) x (elem y z) is thus Bool .

How do types work in Haskell?

In Haskell, every statement is considered as a mathematical expression and the category of this expression is called as a Type. You can say that "Type" is the data type of the expression used at compile time.

How do you check types in Haskell?

If you are using an interactive Haskell prompt (like GHCi) you can type :t <expression> and that will give you the type of an expression. e.g. or e.g.


2 Answers

Well, as said already, $ can be easily understood if you just forget about currying and see it like, say, in C++

template<typename A, typename B> B dollar(std::function<B(A)> f, A x) {   return f(x); } 

But actually, there is more to this than just applying a function to a value! The apparent similarity between the signatures of $ and map has in fact a pretty deep category-theory meaning: both are examples of the morphism-action of a functor!

In the category Hask that we work with all the time, objects are types. (That is a bit confusionsome, but don't worry). The morphisms are functions.

The most well-known (endo-)functors are those which have an instance of the eponymous type class. But actually, mathematically, a functor is only something that maps both objects to objects and morphisms to morphisms1. map (pun intended, I suppose!) is an example: it takes an object (i.e. type) A and maps it to a type [A]. And, for any two types A and B, it takes a morphism (i.e. function) A -> B, and maps it to the corresponding list-function of type [A] -> [B].

This is just a special case of the functor class signature operation:

fmap :: Functor f   =>   (a->b) -> (f a->f b) 

Mathematics doesn't require this fmap to have a name though. And so there can be also the identity functor, which simply assigns any type to itself. And, every morphism to itself:

($) :: (a->b) -> (a->b) 

"Identity" exists obviously more generally, you can also map values of any type to themselves.

id :: a -> a id x = x 

And sure enough, a possible implementation is then

($) = id 

1Mind, not anything that maps objects and morphisms is a functor... it does need to satisfy the functor laws.

like image 21
leftaroundabout Avatar answered Sep 18 '22 12:09

leftaroundabout


I'll start with map. The map function applies an operation to every element in a list. If I had

add3 :: Int -> Int add3 x = x + 3 

Then I could apply this to a whole list of Ints using map:

> map add3 [1, 2, 3, 4] [4, 5, 6, 7] 

So if you look at the type signature

map :: (a -> b) -> [a] -> [b] 

You'll see that the first argument is (a -> b), which is just a function that takes an a and returns a b. The second argument is [a], which is a list of values of type a, and the return type [b], a list of values of type b. So in plain english, the map function applies a function to each element in a list of values, then returns the those values as a list.

This is what makes map a higher order function, it takes a function as an argument and does stuff with it. Another way to look at map is to add some parentheses to the type signature to make it

map :: (a -> b) -> ([a] -> [b]) 

So you can also think of it as a function that transforms a function from a to b into a function from [a] to [b].


The function ($) has the type

($) :: (a -> b) -> a -> b 

And is used like

> add3 $ 1 + 1 5 

All it does is take what's to the right, in this case 1 + 1, and passes it to the function on the left, here add3. Why is this important? It has a handy fixity, or operator precedence, that makes it equivalent to

> add3 (1 + 1) 

So whatever to the right gets essentially wrapped in parentheses before being passed to the left. This just makes it useful for chaining several functions together:

> add3 $ add3 $ add3 $ add3 $ 1 + 1 

is nicer than

> add3 (add3 (add3 (add3 (1 + 1)))) 

because you don't have to close parentheses.

like image 75
bheklilr Avatar answered Sep 18 '22 12:09

bheklilr