简体   繁体   中英

How to Read Type Signatures in Haskell

I would like understand how the type signature works, in other words: I am baffled by the output I get from :type .

 :type (+)
(+) :: Num a => a -> a -> a

Why is it not

 :type (+)
(+) :: (Number, Number) -> Number

instead?

I understand that

(+) :: Num a => a -> a -> a

can be rewritten as

(+) :: Num a => a -> (a -> a)

which means that (+) takes an argument a and outputs a function with type a ->a . Why does it output a function and not a number?

The (+) function is used as a binary operator . Indeed: if you write:

5 + 2

you have, behind the curtains, written:

(+) 5 2

So you call the function with 5 and 2 as arguments. Now in Haskell every function takes exactly one argument as input. So (+) 5 2 is actually a compact notation of:

((+) 5) 2

Indeed, we first takes 5 as argument for the (+) function, and this will produce a new function that takes as input a number and returns a number such that the outcome is five plus that number.

We can thus define a function:

f5 = (+) 5

and then apply f5 to for example 3 and 7 , and we will get f5 3 = 8 and f5 7 = 12 . So if we analyze the types, we will see that:

(+) :: Num a => a -> (a -> a)
(+) 5 :: Num a => a -> a
((+) 5) 2 :: Num a => a

So now we can answer your question:

Why does it output a function and not a number?

If it would immediately output a number, then we could only give it a single number, so in that case we would have written (+) 5 and immediately obtain a number. The only logical way to do that would be to return 5 and thus let (+) act as a unary operator (an operator that takes one parameter into account). But in Haskell, the (+) is a binary operator.

Why is it not

 :type (+) (+) :: (Number, Number) -> Number 

Haskell has a notion of tuples. But again that tuple is a single argument. In case we would construct such function, we would call it with:

f (2, 3)

If f = (+) , then f (2, 3) would result in 5 . But it is not very flexible. A frequent pattern in functional programming is taking a function, and apply a limited number of arguments on it (like we did with f5 = (+) 5 and then pass that function as an argument again. By using tuples, we lose that flexibility.

Since some functions work with tuples, there are two popular functions called curry :: ((a, b) -> c) -> a -> b -> c and uncurry :: (a -> b -> c) -> (a, b) -> c . These functions can transform a given function that takes a tuple as input into a function with two arguments, and vice versa.

For example:

Prelude> :t uncurry (+)
uncurry (+) :: Num c => (c, c) -> c

So by constructing uncurry (+) we have constructed a function that takes as input a 2-tuple.

Note that curry and uncurry work only with 2-tuples (and thus two arguments). There are variants that work with more arguments, but these are not that popular.

Usually using the approach of functions constructing more specialized functions allows more flexibility and thus are more useful.

Say for instance I had an uncurry version of (+) :

uplus :: Num c => (c, c) -> c
uplus = uncurry (+)

Then how would I construct a partially applied function f5 :: Num c => c -> c with that? I could write it like:

f5 :: Num c => c -> c
f5 x = uplus (5, x)

but this requires a lot more syntax. Furthermore if I extend uplus to work with 3-tuples. I have to introduce more variables ( y , z , etc.) to write the partially applied function.

Why is it not

 :type (+) (+) :: (Number, Number) -> Number 

I am assuming that with Number you mean any type in class Num .

This would make ti possible to add an Integer (numeric type) and a Complex Double (another numeric type) and get as a result a Rational . We can't really do that.

We can only sum two values of the same numeric type, and obtain a result of the same type. We need to express that all these three numeric types are equal. This is done by the standard signature:

(+) :: Num a => a -> a -> a

If you instead consider Number as a single, specific, numeric type, then we could not add Int s, since that is a different type. We could remove all numeric types and use only Number , extending it to encompass complexes, rationals, integers, and all other numeric types. The programmer would then lose control of the underlying representation (making it hard to predict performance) and types would be much less precise: we can't guarantee that length :: [a] -> Number does not return a complex number, for instance...

The two types for (+) you suggested are actually isomorphic (the same). To prove two things are isomorphic, we must show that there is a pair of invertible functions which go back and forth between the representations. These functions already exist in the standard library as uncurry and curry

> :t uncurry (+)
uncurry (+) :: Num a => (a, a) -> a

> let add (a, b) = a + b
> :t curry add
curry add :: Num a => a -> a -> a

Proving that curry/uncurry are actually inverses of each other is an exercise for the reader.

To your second question "why does (+) take one argument and return a function rather than a number", this is called partial application. Calling (+) with a single argument returns a partially applied function that captures or "closes over" that argument. For example,

addOne :: Num a => a -> a
addOne = (1+)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM