CS 340 - Lecture 7

Lambda calculus, continued

Currying in λ-calculus

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.

Recursion in λ-calculus

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.

Arithmetic in λ-calculus

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.