I will discuss how multivariate functions are typically worked with in functional programming. In particular, the notion of currying will be explored.
The arity of a function is the number of arguments that the function takes. Functions with no arguments (referred to as nullary functions) have an arity of 0. Single-variable functions are referred to as unary functions and have an arity of 1.
Most functions take more than one variable. Such functions are referred to as multi-arity functions.
In particular, functions which have an arity of 2 or 3 are often referred to as binary and ternary functions, respectively.
be a binary function.
Such a function takes a pair as its argument and outputs an element .
Suppose I want to evaluate at the point .
I could explicitly pass both arguments to and compute the result directly.
Functional programming approaches this from a slightly different point-of-view (for reasons which will be discussed shortly).
Rather than passing the pair to directly, one first passes only to , returning . But is itself a function from . Let . One then passes as an argument to which results in .
In this way the evaluation of the multivariate function at ) is divided into two separate univariate evaluations.
If we let represent the set of all functions from then we can define
to be the curry of .
We want to evaluate
Note that in the first evaluation is the input and the function is the output.
In the second step the function is itself evaluated at resulting
So, that’s currying.
be a ternary function, for some sets .
We will iteratively curry this function.
One can iteratively apply the above argument for arguments. The right-associativity is generally implicitly understood so that the parentheses are normally omitted.
For each of the below examples we will consider a simple addition function function .
add :: (Int, Int) -> Int add(x,y) = x + y
add :: Int -> Int -> Int add x y = x + y
main :: IO () add :: Int -> Int -> Int add x y = x + y add_ = add 1 main = print (add_ 1) -- 2
Uncurried ⟶ curried:
main :: IO () add :: (Int, Int) -> Int add(x, y) = x + y addCurried = curry add addCurried_ = addCurried 1 main = print (addCurried_ 1) -- 2
Curried ⟶ uncurried
main :: IO () addCurried :: Int -> Int -> Int addCurried x y = x + y add = uncurry addCurried main = print (add(1,1)) -- 2
val add = (x: Int, y: Int) => x + y
val addCurried: Int => Int => Int = x => y => x + y
val add: Int => Int => Int = x => y => x + y val add_ = add(1)(_) println(add_(1)) // 2
Uncurried ⟶ curried:
val add = (x: Int, y: Int) => x + y val addCurried = add.curried println(addCurried(1)(1)) // 2
Curried ⟶ uncurried:
val addCurried: Int => Int => Int = x => y => x + y val add = Function.uncurried(addCurried) println(add(1, 1)) // 2
One often sees the notion of partial function application discussed alongside currying. While they may seem similar, in reality they are different.
Currying takes an -ary function and transforms it into unary functions. Partial function application takes an -ary function, evaluates it for some subset of the arguments (say of them) and returns an -ary function.
val add = (a: Int, b: Int, c: Int) => a + b + c val addPartial = add(1, _: Int, _: Int) println(addPartial(1,1)) // 3
Note how partial application mapped a ternary function to a binary function, whereas the curry of
add would map an argument to a unary function to a unary function, as below.
val add: Int => Int => Int => Int = x => y => z => x + y + z val addPart = add(1) val addPart_ = addPart(1) println(addPart_(1)) // 3
Now that we understand what currying is and what it is not, the question remains, why bother currying at all?
In short, currying makes it syntactically easier to work with higher order functions, which will be discussed in a future post.