Skip to content

Function Signatures and Function Definitions

You are probably used to C or Java syntax, which combines the definition of a function with the declaration of its type:

int foo(int x, double y) { ... }

This is a function that takes two arguments of types int and double, respectively, and returns a value of type int. The argument names are x and y.

In Haskell, the declaration of the type of a function and the definition of a function are separate. There are many reasons for this. First, we usually do not declare the type of a function at all. The compiler's type inference mechanism can usually just figure it out. There are three important exceptions to this rule:

  • For top-level functions, ones not defined locally within other functions (within a where or let block of that function), it is good practice to document a function by providing its type signature. Given Haskell's very strong type system, this, combined with a well chosen name of the function, provides a lot of information to the reader of your code about what this function may do.

  • Very rarely, the type inferencer is unable to infer the type of a function or draws the wrong inference. In that case, it needs our help, and we need to specify the type of the function explicitly.

  • The type inferencer always infers the most general type of a function that makes the program correct, when this is possible, when the program is actually correct. Sometimes, this is undesirable. Sometimes, we want a more restricted type of a function, mainly because it allows the compiler to generate more efficient code. So we need to restrict the type of the function using an explicit type signature.

The second reason why the declaration of a function's type and its definition are separate is that the definition of a single function in Haskell may consist of multiple equations, multiple definitions that specify the value of a function under different conditions. However, the function has only one type, so we specify the type once and then list the different equations that define the function.

A function definition looks like this:

add x y = x + y

This is a function with two arguments, named x and y. Its value is the sum of x and y, specified to the right of the equal sign.

In Haskell, the arguments of functions are not enclosed in parentheses nor separated by commas. Instead, similar to the syntax of your favourite Unix shell, function arguments are separated from the function name and from each other by spaces, both in the definition of a function and when calling it:

GHCi
>>> add x y = x + y
>>> add 2 3
5

If an argument to a function is a more complex expression, it needs to be enclosed in parentheses:

GHCi
>>> add (1 + 1) 3
5

The type of our add function is1

add :: Int -> Int -> Int

This says that add takes two arguments, both of type Int, and its return value is again of type Int. In general, type declarations have the function's name on the left, followed by ::, followed by the type of the function. In this type signature, the arguments and the return value are separated from each other by the symbol ->. If you read -> as an arrow, then for a function with a single argument, this notation mirrors closely the way we write function types in mathematics: \(f : X \rightarrow Y\) becomes f :: x -> y in Haskell. For multi-argument functions, we will see that they are in fact an illusion in Haskell. The type of the add function above is really Int -> (Int -> Int) once fully parenthesized. Thus, it is a function that maps an Int to another function, and that function maps an Int to an Int. This is not simply a weird way to look at multi-argument functions; it has powerful consequences for how we can use multi-argument functions in Haskell, which we will discuss soon.


  1. Actually, the type is more general. It is add :: Num a => a -> a -> a. This says that we can call add with arguments of any type, as long as both arguments have the same type a. The return value then has the same type a. In addition, the Num a => part says that a can't actually be any type. It must be a type in the Num type class. We'll talk about type classes later. They are essentially the same as interfaces in Java or traits in Rust. They guarantee that a given type that implements a given type class, interface or trait supports a specific set of operations. Here, the type inferencer concluded that a must be an instance of Num because the add function uses the addition operator, which is one of the functions provided by the Num type class.