Beginner question, is haskell ignoring type declaration when writing function?

Hello all,

I am really curious about a simple 3 line code that I wrote then ran in ghci. I understand what is happening, but don’t know why under the hood it is happening.

When I write this file:

createAddFunc :: Int -> (Int -> Int)

createAddFunc x y = x + y

adds5 = createAddFunc 5

and then run ghci and load this file, something unexpected happens.

The createAddFunc declaration should be saying that createAddFunc takes an Int and returns a function that accepts an Int and returns at int, itself.

When I use adds5 it works as expected.

Except I tried:
createAddFunc 1 2
and too my surprise, it returned 3. (instead of an error)

This would make sense without the type declaration as my function definition could read as accept 2 Ints and return them added together.
However my definition I would have thought would make it clear that I don’t want that. I should be accepting 1 int only.

My guess is the parenthesis are just white noise? As in the declaration to the compiler reads Int → Int → Int and then depending on context the compiler decides if a function was meant or adding was meant? Is this guess correct? Or what is going on under the hood?

It’s not ignoring the type but yeah

:: Int -> (Int -> Int)
-- is equivalent to
:: Int -> Int -> Int

When you do createAddFunc 1 2 you can think of it as doing

createAddFunc 1 2
= (createAddFunc 1) 2
= (\y -> 1 + y) 2
= 1 + 2
= 3

also note that

(Int -> Int) -> Int
-- is not equivalent to
Int -> Int -> Int
1 Like

You have discovered partial application!

-> is right associative, which means

a -> b -> c -> d

is

a -> (b -> (c -> d))

So Every Haskell takes exactly 1 argument and returns something else (whether an Int in a -> Int or another function in a -> b -> c — which is the same as a -> (b -> c)). «add takes two parameters» is a convenient shorthand but — as you have discovered — not precise.

Partial application comes extremely handy in higher order functions:

> map (+2) [1..3]
[3,4,5]
-- no need to define a `plusTwo` function

and part of what makes Haskell cosy.

1 Like

This is an example of currying and partial application.

Note from the first link:

In Haskell, all functions are considered curried: That is, all functions in Haskell take just one argument.

This probably sound strange, but it makes sense when you remember that Haskell has first class higher-order functions. Instead of having a function that takes multiple arguments, a curried function takes one argument and returns a function that waits for the rest of its arguments. So in the case of your code,

createAddFunc :: Int -> (Int -> Int)

is a function that takes one integer as an argument and returns a function that takes one integer as an argument and returns an integer. Or more clearly, it is a function that takes one integer as an argument and returns (a function that takes one integer as an argument and returns an integer). When you have a function in Haskell that looks like it takes more than one argument, meaning it has more than one variable name on the left hand side of the =, that means the function is curried. It will take one argument and then return a function waiting for the rest of its arguments until it has them all and then returns the non function return value.

This is very useful because it means we can easily do partial function application, which is what you did with adds5. adds5 is exactly what createAddFunc should return when given one Int argument: a function waiting for an Int which when given one will return an Int.

So, no, Haskell isn’t doing any magic and deciding on its own whether or not you meant to return a function or to return some other value.

The reason why

Int -> (Int -> Int)
 --and 
Int -> Int -> Int

are the same in this case is because, if you think through the process of currying single value arguments, the parentheses will associate automatically from the right.

foo :: Int -> Int -> Int -> Int -> Int -> Int

Is a function which takes an Int and returns a function which takes an Int which returns a function that takes an Int which… Note that it takes an Int first and returns a function, so we could have written

foo :: Int -> ( Int -> Int -> Int -> Int -> Int)

so what would the type of foo 1 be?

foo 1 :: Int -> Int -> Int -> Int -> Int

which of course by the same logic we could have written

foo 1 :: Int -> (Int -> Int -> Int -> Int)

and I’m sure you can follow through with the rest of the reductions that happen when we add more arguments. When you get to the end, you find that the original function could have been written

foo :: Int -> (Int -> (Int -> (Int -> (Int -> Int))))

But that would be way too much to write every time, so Haskell automatically associates functions from the right, leading to us being able to write function definitions without needing to place all of the parentheses.

However, note that parentheses do matter in general. You can’t just add them in randomly, because it will change some values from being things like Ints to being functions themselves and then the compiler will complain that you gave it an Int instead of a function. The times when you don’t need to write the parentheses are exactly when everything is right associative. For example consider a function which takes an Int, a function taking an Int and returning an Int, and then applies that function to the first argument:

bar :: Int -> (Int -> Int) -> Int
bar x f = f x

bar 3 (+3) 
6

If you tried to write that function without parentheses

bar' :: Int -> Int -> Int -> Int
bar' x f = f x

you would get a compiler error for a few reasons, one of which being that the body doesn’t reflect what the type says, which is shows that bar and bar’ have different types. You can see why by putting in our associative parentheses:

bar :: Int -> ((Int -> Int) -> Int)

bar' :: Int -> (Int -> (Int -> Int))

and those are clearly not the same function. In our attempt to define the body of bar’, we would be applying an Int to an Int and expecting it to return to us a function waiting for an Int to then return us another Int. And the compiler is not happy with that.

So, parentheses are not just white noise, they matter a lot. They can change the entire meaning of a function if you group them differently in most cases. However, because Haskell is curried by default, all multi parameter functions are really higher order functions, and the types automatically associate from the right. You can always leave those parentheses out, but those are the only ones you can leave out.

2 Likes

Ahh that is very fascinating! Thank you eddiemundo, f-a, and KripkesBeard! I have learned a lot about haskell from a simple function haha.

1 Like