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
>>> 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:
>>> 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
>>> 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.