Skip to content

Multi-Argument Functions are an Illusion

Let's look at our multiply function again:

multiply :: Int -> Int -> Int
multiply x y = x * y

Its type looks weird. Why not (Int, Int) -> Int? After all, it takes two integers (a pair of integers) as arguments and maps them to a single integer as its result. We could have defined multiply this way, only we would have had to write its arguments as a pair as well:

multiply :: (Int, Int) -> Int
multiply (x, y) = x * y
GHCi
>>> multiply (x, y) = x * y
>>> multiply (3, 5)
15

But that's a different function. This one takes a single argument, which happens to be a pair of integers. We'll talk about tuple types later in this book.

You may think that I'm splitting hairs now. Logically, a function that takes a pair of integers as argument is the same as a function that takes two integers as arguments. The difference is that, in Haskell, a function that takes two integers as arguments isn't really a function with two arguments at all. You'll see this if you introduce the appropriate parentheses in the type signature of multiply:

multiply :: Int -> (Int -> Int)

This says that multiply takes one argument, which it maps to another function. This works only because functions in Haskell are ordinary values that can be returned as the result of other functions. What does this other function do? It takes an integer and maps it to an integer. Now it suddenly makes sense that argument types in the type signature aren't separated by commas but by the same symbol (->) that separates arguments and the return value.

If we want to make sense of this new view of our multiply function not as a two-argument function but as a single-argument function that returns another function as its result, we should also see whether we can interpret the function definition and function application in this fashion. If multiply is a function that returns another function, then our definition should express this. Observe that in the type signature above, we introduced parentheses from right to left. A function of type a -> b -> c -> d would really have type a -> (b -> (c -> d)), that is, it is a one-argument function that turns an argument of type a into another function. That function takes an argument of type b and returns another function. That one, finally, takes an argument of type c and turns it into a value of type d. In the function definition and when applying a function, we apply parentheses from left to right. So our definition

multiply x y = x * y

is really

(multiply x) y = x * y

And that's consistent with the type of multiply. multiply x is a function of type Int -> Int. Given its argument y, it produces the value x * y.

The same works for function application. When we call the function multiply 3 4, this should be read as (multiply 3) 4. We apply the function produced by multiply 3 to the argument 4.

Now I hope you had a nagging question while reading all this. If multiply 3 is a function of type Int -> Int, what would happen if we didn't provide the second argument? What would we get? Quite naturally, we'd be left with a function of type Int -> Int. That's what our type definition says. Since functions are plain old values in Haskell, we can take this function and assign it to a variable:

GHCi
>>> multiply x y = x * y
>>> triple = multiply 3
>>> quadruple = multiply 4
>>> triple 5
15
>>> quadruple 5
20

multiply 3 is a function that multiplies its argument with 3. We assign this to the variable triple, and then we can call triple 5, triple 1000, and so on to triple these values. Similarly, multiply 4 is a function that multiplies its argument with 4, and we assigned this function to the variable quadruple so we can call quadruple 5, quadruple 1000, and so on.

And this is why writing multi-argument functions as a -> b -> c -> d instead of (a, b, c) -> d is useful. For a function of type a -> b -> c -> d, we can provide fewer than three arguments and hold on to the resulting function. This function can then be called by providing the remaining arguments. A function of type (a, b, c) -> d expects a single argument, a triple whose components have types a, b, and c, and we need to provide this argument in its entirety if we want to call the function—we need to provide all three components of the triple.

This technique of providing only a subset of the arguments of a multi-argument function is called partial function application. It may seem like a cute little trick for now, one that's nice to have but also one one can do without. As we will see later, the ability to apply functions partially allows us to express some code much more succinctly than if we didn't have partial function application.

As a final comment on multi-argument function, remember that our definition of multiply,

multiply x y = x * y

is just syntactic sugar for

multiply = \x y -> x * y

Well, this lambda expression sure looks like the function actually takes two arguments. Once again, this itself is syntactic sugar for

multiply = \x -> \y -> x * y
GHCi
>>> multiply = \x -> \y -> x * y
>>> multiply 5 3
15

So multiply is the name we give to the function \x -> \y -> x * y. This is a function that takes one argument, x, and turns it into another function. That function takes one argument, y, and turns it into the value x * y. So it's really anonymous one-argument functions throughout, and we obtain named functions by binding anonymous functions to names.