Lambda Calculus
Lambda calculus is a model of computation. It was introduced in the 1930s by Alonzo Church. He was trying to develop formal foundations for mathematics. His attempts were unsuccessful, as several of his systems turned out to be inconsistent. Nevertheless, he invented lambda calculus as a fragment of his formal system. Using this calculus, he solved the famous Hilbert's Entscheidungsproblem by showing that there are questions that cannot be solved algorithmically. Later, lambda calculus became a formal tool for analyzing computability and functional programming languages.[1]
Lambda calculus is probably the simplest universal programming language. Many of the key concepts that make functional programming so powerful today were invented for, or are inspired by lambda calculus. This includes for example currying, recursion, and closures. You will also see were all this parenthesis in Racket come from!
Lambda calculus can be viewed as a minimal programming language in which computations can be described and as a mathematical object about which rigorous statements can be proved. It serves as a core language that allows one to specify and study programming language features by translating them into the core language. We will discuss only the untyped variant of lambda calculus. There is also a typed version used in the study of type systems.[2]
Even though the syntax and semantics of lambda calculus are extremely simple, lambda calculus is Turing complete. In other words, it is possible to simulate any Turing machine with a program in lambda calculus and vice versa.
Syntax
The syntax of lambda calculus has only three types of terms: a variable (denoted by lowercase letters
The abstraction
Note that each abstraction and application introduce parentheses. To simplify the notation, we use several conventions to remove some of the parentheses.
- We often leave the outermost parentheses.
- The application is left-associative, e.g.
is - The bodies of functions extends to the right as far as possible.
Using the above conventions, we can simplify the following term
as follows:
The red parentheses are the only ones that remain because they determine the body of the anonymous function defined by
The parentheses are necessary if we want to represent lambda terms as sequences of characters. Alternatively, we avoid parentheses if we understand lambda terms as abstract syntax trees. The above lambda term
Before focusing on semantics, we need to introduce the scopes of variables. An occurrence of a variable
Semantics
In lambda calculus, programs (i.e., lambda terms) consist of anonymous functions created by abstraction and function calls. The computation of such a program is the simplification process reducing the program to a lambda term that cannot be reduced anymore. This term is the value computed by the program.
A lambda term
(lambda (x) t)
We can call the function
The
Let us see some examples. We denote the equality on lambda terms by
Note that we substitute only for the free occurrence of
The substitution rule has one more caveat regarding the variable names. Consider a redex
The above reduction step is not valid. The reason is that functions do not depend on the names of their arguments. Consider the following Racket program:
(define y 5)
(define (cadd x) (lambda (y) (+ x y)))
If we now call
(cadd y)
the resulting function cannot be
(lambda (y) (+ y y))
which would be doubling its argument. Instead, it should be the function adding
> (define (cadd x) (lambda (z) (+ x z)))
> (cadd y) => (lambda (z) (+ y z))
(lambda (z) (+ 5 z))
Thus the correct reduction of the above lambda term should proceed as follows:
Whenever we want to substitute a term
Formally, we can define the substitution by induction as follows:
The last case is the one where we need to do the
Church-Rosser Theorems
The computation in lambda calculus is performed by
This leads to natural questions: Does it matter which redex we reduce first? Can different reduction orders lead to different final values? Can some reduction orders terminate, whereas some do not? The answers to these questions are provided by the Church-Rosser theorems.
Before we state the Church-Rosser theorems, let us discuss a few examples showing what might happen. First of all, it may happen that the reduction process does not terminate no matter which reduction order is used! Consider the following term containing only a single redex:
The above lambda term corresponds, in fact, to an infinite loop.
For some expressions, the reduction process terminates for some reduction orders and diverges for others. For example, the term
contains two redexes. The inner one whose reduction does not terminate. If we reduce the redex applying
A lambda term is said to be in normal form if it is irreducible, i.e., it contains no redexes. Reduction orders are also called evaluation strategies. Several of them were investigated. Let us introduce two of them:
- Normal order reduces the leftmost outermost redex first at each step.
- Applicative order reduces the leftmost innermost redex first at each step.
We say a redex is to the left of another redex if its lambda appears further left. The leftmost outermost redex is the leftmost redex not contained in any other redex. The leftmost innermost redex is the leftmost redex, not containing any other redex. For example, consider the following lambda term:
It has five redexes depicted in the following figure by the red color.
The leftmost outermost redex is
Let's see how the above lambda term would be evaluated by the evaluation strategy following the normal order. The reduced redexes are denoted by the line:
Finally, we get to the Church-Rosser theorems. The first theorem states that no matter which reduction order we choose, we will always get the same normal form provided the reduction process terminates. The second theorem states that the normal order always terminates, provided a sequence of
Church-Rosser Theorems:
- Normal forms are unique (independently of evaluation strategy). Consequently, a lambda term cannot be reduced to two different normal forms.
- Normal order always finds a normal form if it exists.
Programming in lambda calculus
Lambda calculus has no numbers, arithmetic, Booleans, etc. It has only anonymous functions and function calls. Thus it might look pretty useless. However, lambda calculus is Turing complete. So it should be no surprise that we can encode all the things like numbers and arithmetic. We will build a few basic encodings from scratch in the following sections. Besides numbers and arithmetic, the most interesting thing is how to represent recursive functions in lambda calculus. That is not straightforward, considering there are only anonymous (nameless) functions.
Although lambda cannot internally introduce names for terms or functions, we can do it in our exposition to simplify the notation. We will denote specific lambda terms (usually combinators) with uppercase letters. For instance, the identity function is denoted by
We will also introduce one more convention for writing lambda terms. Each function defined by abstraction is unary. However, we can have functions of higher arity due to currying. For instance, a function of two arguments
Booleans
We start with Boolean values and operations. Since Boolean values often appear in conditional expressions, we model them so that they work directly as the if-then-else expression. The if-then-else expression consists of a condition (i.e., a term whose value is either to true or false) and two expressions to be evaluated depending on the result of the condition. Thus we model Boolean values as binary projection functions:
Boolean values are functions of two arguments returning one of them. More precisely,
Considering the encoding of Boolean values, it is easy to encode basic Boolean operations conjunction
Let us check that the encoding works correctly. For an arbitrary lambda term
Similarly, we can define disjunction that is true if the first argument is true and the value of the second argument otherwise.
For an arbitrary lambda term
Finally, it is straightforward to encode negation:
Numbers and arithmetic
To encode numbers and arithmetic operations, we use the so-called Church numerals. They encode a natural number
Note that
Using the above encodings for numbers, one can easily define the successor function
The input number
Once we have the successor function
For example,
Multiplication of two numbers
Since
The multiplication term
We can remove the variable
we end up with
In the following section on recursive functions, we will need the predecessor function on natural numbers
Thus the pair
(define (point x y)
(lambda (m) (m x y)))
Given a pair, we can access its components by applying it to the projection functions
To create the predecessor function, we need to find a term such that if we apply it
The function
Consequently, we can define the predecessor function as the function applying
Zero test
To implement a recursive function, we need a condition when to stop the recursion. In the example we will discuss in the next section, we test whether a given number is zero. To check whether a given number is zero, recall that
So we have
and for
Recursive functions
Finally, we are getting to the most exciting construction of how to encode recursive functions if we have only anonymous functions in lambda calculus. Recursive functions can be defined through the so-called
Note that it is an expanded version of the term
We obtained a term denoted
Whether this process stops or not depends on
To see an example, we implement a function computing for a given natural number
(define (sum-to n)
(if (= n 0)
0
(+ n (sum-to (- n 1)))))
We must apply the sum-to
, but we replace the recursive call with a call of a function given as an argument.
The function tests if
Now it remains to turn
Let us see if it correctly sums up all the natural numbers to
Note that the expression in the last line contains several redexes. The one hidden in
If you are interested in the history, check the entry on Alonzo Church in the Stanford Encyclopedia of Philosophy. ↩︎
Benjamin C. Pierce: Types and programming languages. MIT Press 2002, ISBN 978-0-262-16209-8, pp. I-XXI, 1-623. ↩︎
Note that the terminology on bound and free occurrences is completely analogous to the terminology used in first-order logic, where variables can be bound by quantifiers instead of
. ↩︎ We will abuse the notation and write
even if several -reductions are needed to get from to . ↩︎