The Lambda Calculus as a Foundation for Computing |
[Note: I'm going to omit the "@" symbol for function application in this section, as it just clutters up the formulas.]
The lambda calculus was invented / designed by Alonzo Church and his student Stephen Kleene at Princeton in the early 1930's. Church showed the undecidability of validity in the First-Order Predicate Logic in 1936 using the lambda calculus (Turing showed the same result independently shortly thereafter). Turing later showed the equivalence of the lambda calculus and Turing machines in computational power. Church is responsible for Church's thesis, which states that every computable function is representable as a term of the lambda calculus (equivalently, as a Turing machine).
The (pure) untyped lambda calculus has the following very simple description as the language generated by:
Term | => | v | λ v. Term | (Term Term) |
where v represents any variable symbol. Thus terms are built up from variables, lambda abstractions, and function applications. Because any term may be applied to any other term, all terms of the lambda calculus may be interpreted as functions.
Because lambda expressions are supposed to represent all computable functions - in particular all computable functions from natural numbers to natural numbers - there must be a way of encoding natural numbers as lambda expressions. Below we provide the simplest way of providing this encoding, but we start first with booleans.
We encode true, false, and a conditional expression as follows:
true | = | λ u. λ v. u. |
false | = | λ u. λ v. v. |
cond | = | λ u. λ v. λ w. u v w. |
True is a function that takes two arguments and returns the first; false takes two arguments and returns the second. Cond takes three arguments and applies the first to the second and third. Notice what happens if we apply Cond to three arguments where the first is either true or false.
cond true M N | = | true M N |
= | M. |
while
cond false M N | = | true M N |
= | N. |
Thus cond in association with true and false works as an if ...then ...else ... expression.
If we define:
Pair | = | λ m. λ n. λ b. cond b m n. |
then we can think of Pair M N as representing the pair of M and N. Elements of the pair can be extracted by applying Pair M N to true to get the first component, M, and false to get the second component, N. Thus we can also define fst = true and snd = false.
In thinking about how to encode natural numbers, it is helpful to think of the standard way of building up numbers using 0 and a successor function, Succ, defined so that Succ n = n. If our language included symbols representing 0 and the function Succ, we would have no difficulty building up the natural numbers as 0, Succ 0, Succ( Succ 0), Succ( Succ (Succ 0)), etc. We can emulate this in the lambda calculus by representing numbers as functions, which, if provided with 0 and Succ, would result in the encoding just given.
Thus we write:
0 | = | λ s. λ z. z. |
1 | = | λ s. λ z. s z. |
2 | = | λ s. λ z. s (s z). |
... | ||
Thus, for example, 2 Succ 0 = Succ (Succ 0) = 2. For any n and any lambda terms s and z,
n s z | = | s (s (s ( ...(s z) ...))), |
where the right hand side includes n occurrences of s.
We can write the successor function for this representation of numbers as follows:
Succ | = | λ n. λ s. λ z. s (n s z). |
Thus Succ n = λ s. λ z. s (n s z), which is in the correct form to represent a number. Also as we saw above, n s z evaluates to n applications of s to z, so s (n s z) represents n+1 applications of s to z. Thus Succ n represents the same function as n+1.
Addition and multiplication are now rather simple:
Plus | = | λ n. λ m. λ s. λ z. m s (n s z). |
Mult | = | λ n. λ m. m (Plus n) 0). |
Thus Plus n m = λ s. λ z. m s (n s z), which is a function of s and z that simply applies s a total of m times to the nth successor of z. This results in a total of m+n applications of s to z, which is the same as m+n.
Mult n m = m (Plus n) 0), which adds n to 0 m times, giving m+n. We can define exponentiation similarly.
That should give us some confidence that we can define a lot of functions. However, there are two important pieces missing. Can we represent the predecessor function and can we define functions recursively?
Church and Kleene came up with the above functions pretty quickly, but predecessor was a real sticking point. Kleene worked on it for a long time with no success, but then one day at the dentists, while having his teeth worked on, he figured out a clever way of solving the problem.
The difficulty is that one cannot remove occurrences of s from the representation of n. Thus somehow, one must figure out a way to compute n-1 from n. The way Kleene came up with was to build up a series of pairs of the form <n-1,n> so that n-1 could be removed from the pair at the end.
Start by defining PZero = <0,0> = Pair 0 0. Now define PSucc = λ n. Pair (snd n) (Succ (snd n)). Thus PSucc ignores the first item of the pair, moves the second to the first position and then puts the successor of the second element into the second position. It is easy to see that PSucc PZero = <0,1>, and, in general, n PSucc PZero = <n-1,n> for n > 0.
It is now easy to define the predecessor function:
Pred | = | λ n. fst (n PSucc PZero). |
Thus Pred n = fst (n PSucc PZero) = fst <n-1,n> = n-1 for n > 0. Note that Pred 0 = 0. This is OK, because there is no predecessor for 0 in the natural numbers.
The Lambda Calculus as a Foundation for Computing |