We saw previously that λ-calculus (a.k.a. Lambda calculus) can
define functions with a single
parameter. However, in real programs, we would often like to
write functions that take more than one parameter.

We can emulate functions with multiple parameters in
λ-calculus as a
series of applications of one-parameter functions. This
technique
is called currying
(named
after the logician Haskell
Curry.)

Let's define a function to add two numbers. The
application of
the function should look something like this

f a b

where "f" is a function and a and b are numbers that we want
to
add. The result of this function should be the sum of a and b.

Expressions in λ-calculus are left-to-right associative; that
means
that the expression above should be evaluated as

(f a) b

In other words, the function application (f a) is evaluated
first,
returning a function which is then applied to b.

Here is how we can define f

(λ x . (λ y . x + y))

This function should be understood as follows:

It is a function that takes
a single
parameter called x

Its body is a function that
takes a
single parameter y and whose body adds x and y

In other words, it is a function that, when applied, evaluates
to
another function! This idea may seem odd; if we were
programming
in C or C++, it would be like returning a function as a return
value. In fact, this is precisely what λ-calculus allows;
functions can be used as values.
The ability to treat functions as values is one of the characteristics
of functional
programming languages,
as we will see later in the semester.

Let's see the entire function in action adding the numbers 2
and
3. We will set it up in the form

(f 2) 3

where f is the function we specified above.

((λ x . (λ y . x + y)) 2) 3

Recall that λ-calculus works by repeatedly expanding function
applications by replacing the entire expression with the body of the
function, substituting the argument value for each occurrence of the
parameter in the applied function's body.

We'll start by expanding the first function application:

((λ
x . (λ y . x + y)) 2)
3
<----- replace each occurrence of x in the function body with 2

(λ y . 2 + y) 3

This leaves us with one more function application to evaluate:

(λ y . 2 + y) 3
<--------
replace each occurrence
of y in the function body with 3

2 + 3

So, the original expression eventually expands to "2 + 3".

As a shorthand, we will write

(λ x y . body)

instead of

(λ x . (λ y . body))

when we want to define a function with two parameters, x and y.

So far we have seen how to write λ-calculus functions and apply them to values. What if we want to write a recursive function: one that can call itself? Because there are no looping constructs in λ-calculus, we need recursion in order to express algorithms that require an unbounded series of steps to compute a result. (Any algorithm using a loop can be rewritten to use a recursive function.) If we can express recursion, then λ-calculus is Turing-complete, meaning that any algorithm can be expressed in λ-calculus.

Here is an example of a recursive function in C:

// Computes n! = 1 * 2 * 3 * ... * n

// We assume n > 0

int factorial(int n)

{

if (n == 1) {

return 1;

} else {

return factorial(n - 1) * n;

}

}

A recursive function works by progressively breaking down an
overall
problem into a series of simpler sub-problems until it reaches a base case that can
be solved
without recursion.

So, how can we write a recursive function in the λ-calculus
when our
functions don't have names? On the surface, this seems like
an
insurmountable problem. However, it can be solved through the
use
of a special construct called the Y-combinator.

Let's define a version of the factorial function in
λ-calculus. We will use an extended syntax allowing some new
constructs, such as

Functions with multiple
parameters

Boolean logic and
conditional evaluate
(true, false, if/then/else)

We have already shown that functions with multiple parameters
can be
implemented through currying. Boolean logic and conditional
evaluation can also be expressed in λ-calculus (the Wikipedia
article on λ-calculus has details).

fact = (λ f n . if
n==1 then 1
else (f (n -
1)) * n)

We will abbreviate this function as fact. Note that
this
function has two parameters, f and n.

This isn't quite a recursive function: if the parameter n is
not 1
(meaning that the base case hasn't been reached), then the function f
passed as a parameter is applied to solve the recursive
subproblem. In other words, we can use this function as a
recursive function as long as the parameter f expands to precisely the
same function (as many times as necessary)!

The Y-combinator manages this trick of allowing a function to
refer
to itself via a parameter. The Y-combinator can be defined in
λ-calculus as

Y = λ g . (λ x . g (x x)) (λ
x . g (x x))

We will abbreviate this function as Y.

Let's see what happens when we evaluate the expression "Y fact"

Y fact

(λ g . (λ x . g (x x)) (λ x
. g (x x)))
fact
<---------- substitute fact
for g in body

(λ x . fact (x x)) (λ x .
fact (x
x))
<----------
substitute argument for x in body of first function

fact ((λ x . fact (x x)) (λ
x . fact (x
x)))

This final expression is equivalent to

fact (Y fact)

In other words, each time we apply the Y combinator to a
function,
it expands one instance of the function, while generating another
application of itself to the function, which may then be used
recursively.

We can see Y and fact in action by evaluating the expression "fact (Y fact) 3"

fact (Y fact) 3

if
3==1 then 1
else ((Y
fact) (3 - 1)) * 3

(fact
(Y fact) 2) * 3

(if
2==1 then 1
else ((Y
fact) (2 - 1)) * 2) * 3

((fact (Y fact) 1) * 2) * 3

((if
n==1 then 1
else ((Y
fact) (1 - 1)) * 1) * 2) * 3

((1) * 2) * 3

Note that even though we are using the names fact and Y in the
example above, they are really just abbreviations for the expressions
we defined earlier. If we wanted to we could write out this
example without the abbreviations, but it would be tedious and hard to
understand.

In general, any recursive function can be written in a form
that may
be used with the Y-combinator.

Note that we have been assuming that integers and arithmetic
operators are part of λ-calculus
that is unrelated to functions and function applications. In
fact, it is possible to define
integers and arithmetic (addition, subtraction, etc.) using just
functions and function applications. In other words, the
expression

2 + 3

could be written as an expression consisting of just functions
and
function applications. Essentially. each integer is
represented
by a function. We can define mathematic operations on
integers
using recursive functions. So, although we won't try to fully
elaborate this idea here, numbers and other kinds of data can be built
out of functions.