Haskell from the ground up

Functions you write yourself

How a function definition is written, how you apply a function to its arguments, and why a type line is a promise worth writing down.

Part 2 of 12

Beginner15 min readBack to Courses

A definition is a promise about types

A function definition has three parts: the name, a line saying what goes in and what comes out, and the equation that says how. The type line is a promise, and the compiler checks the equation against it, so a mistake comes back as a message rather than as a surprise later.

Read Int -> Int as: give me an Int and you get an Int back. Two arrows mean two arguments, and the type after the last arrow is always the type of the answer. The signature is not a comment: change double :: Int -> Int to double :: Int -> Double and the equation n * 2 no longer fits, so the compiler rejects the pair before anything runs.

The answer is the last expression

There is no return statement in this language, and nothing to assign to. A function is one expression, and the value of that expression is the answer. When the body needs several steps, they are named with let or where and the final expression uses them; the last line is what comes back.

That is why an if has to have an else here. If the branches were statements, the missing one would simply do nothing; because the whole if is a value, a missing branch would leave the value undefined, and the compiler refuses that. The same rule explains why a case needs every constructor: each one has to produce an answer of the one declared type.

Applying a function is a space

You apply a function by writing its name and then the arguments, separated by spaces: add 2 3. There are no brackets around an argument list and no commas between arguments, because brackets and commas belong to tuples and lists.

Because the arrows group to the right, add 10 is a perfectly good value on its own: a function waiting for one more number. Naming that value, as addTen does below, is how a general function becomes a specialised one.

Three small functions

double :: Int -> Int
double n = n * 2

shout :: String -> String
shout word = word ++ "!"

area :: Double -> Double
area side = side * side

Two arguments, one answer

Run it, then add a function of your own that takes a name and an age and answers a line of text for them.

Output
Click "Run" to execute your code

A name in lowercase is a value or a function, and a name that starts with a capital letter is a type or a constructor. The type line can be left out when the compiler can work it out, but writing it down is the quickest way to find out that you meant something else.

Brackets group, spaces apply

Application has the tightest grip of any operator. double 3 + 1 is (double 3) + 1, and double (3 + 1) is the call you meant when the sum is the argument. The brackets are not part of the call syntax; they group the expression that should arrive as one value.

The same trap waits inside a call. line "Ada" (add 40 55) passes the answer of add as the second argument. Written as line "Ada" add 40 55, the function itself arrives where a number was expected, and the compiler says so.

It is also why there are no commas. A comma builds a tuple, so add 2, 3 is read as a pair rather than a call, and add(2, 3) is a call to add with one pair.

The type line is checked, not trusted

When an equation and its type line disagree, the compiler points at the equation. That is useful, because the message lands on the branch that answers the wrong type rather than on the promise above it.

Every equation for one name has to answer the same type, and that type is the last one in the signature. A helper that answers text in one case and a number in another is not a function with two moods; it is a mistake the compiler can see before the program runs.

Leaving the signature out does not remove the check. The compiler works out a type of its own, sometimes a more general one than you intended, and the error then surfaces at the first call that disagrees with it — further from the cause.

Three mistakes are worth recognising. Writing add(2, 3) borrows the brackets and comma of another language and is read as a call with one tuple argument. Starting a function name with a capital letter makes it a constructor, so Double n = n * 2 is rejected. And a signature with one arrow too few is caught at the definition, which is exactly the moment you want to hear about it.

Try these next

  • Write line with the arguments in the other order, then update every call to match.
  • Define area :: Double -> Double for a rectangle that takes a width and a height, and use it inside a sentence.
  • Give add a signature with one arrow too few, read the error, then put it back.

One call inside another

Run it and read the five lines. Then add `triple :: Int -> Int` and use it inside `line` for a score of your choosing.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

How do you apply add to the numbers 2 and 3?