Lambda Calculus
The section on computability mentioned the Church-Turing Thesis that Turing machines fully capture our notion of what may be computed.1 Part of the original justification for this belief is that Alan Turing showed in 1936 that his machine model was equivalent in power to another model of computation being developed at around the same time by Alonzo Church. This model is now known as the lambda calculus, and it forms the core of the theory of functional programming.
As already mentioned in the introductory section on functional programming, the symbol lambda () is used to
indicate an anonymous function defined by an expression. For example, is the function that takes any argument and
returns that argument plus one; in ReasonML, this would be written x => { x + 1 }
.
The fundamental operation in -calculus is known as beta () reduction. Given an expression
that contains a function applied to an argument, reduction allows the application to be replaced by the body of the function
with the argument substituted for the variable.2
For example, may be reduced to the expression . To avoid an explosion of parentheses, we will often write a lambda function with a dot after the parameter name, meaning that the body of the function extends as far as possible to the right within the surrounding expression. We will also often omit the parentheses around the argument in a function application, writing instead of . When a term is applied to multiple arguments we will associate the application to the left, so that is the same as . With these conventions, the previous example becomes .
Formally, the lambda calculus deals with terms of just three forms:
- variables (, , …),
- applications (, where and are arbitrary lambda terms), and
- abstractions (, where is a variable and is a lambda term). Although the example above used additional symbols such as and , these are not necessary. The remainder of this section will show how to encode common data types and operations with lambda terms. The development is based on the Church encoding as presented in the Wikipedia article on the lambda calculus; other encodings are possible.
Church Numerals and Arithmetic
We may encode the natural numbers as follows:
That is, the encoding of is a function that takes a function and a value and iteratively applies to a total of times. The successor function, , is then given by . Using this, we may define addition by noticing that adding to is the same as taking the successor of , times:
Similarly, since multiplication is repeated addition, we may define
For example,
If we had a function PRED that computed the "predecessor" (, except the predecessor of zero is zero because we are limiting our arithmetic to the natural numbers for the moment), then we could similarly define subtraction as the iteration of PRED:
Note that is zero whenever ; this is often called the "modified minus" or monus operation. The function PRED itself will be defined below.
Booleans and Conditionals
Fundamentally, the role of a Boolean value is to make a distinction: true or false, yes or no, do this or else do that. The encoding of a Boolean will therefore take two arguments, and , and select one of them:3
Given these encodings, we may then define Boolean operations and the conditional as follows:
You should convince yourself that these operations are correct, by evaluating each possible combination: , …. Note that IF is trivial, since is the same as just applying ; essentially, the meaning of a Boolean is the conditional, just as the meaning of a natural number is the ability to iterate a particular number of times.
The most basic operation on natural numbers that returns a Boolean is the predicate ISZERO, which takes a number and returns TRUE if it is zero and FALSE otherwise. This is easy to implement as the -fold iteration of the constant FALSE function, starting from the initial value TRUE:
If is zero, then we just get TRUE. If is non-zero, then the constant FALSE function will be applied at least once, making the result FALSE.
Building on this, we get less-than-or-equal-to and equality predicates by using subtraction:
This encoding of Booleans is actually found in some pure object-oriented languages, such as Smalltalk. The idea is that, if everything is an object, then the natural way to do anything is to call a method on an object. The TRUE and FALSE objects have methods that ask them to choose one of two things; the method in TRUE is implemented to return the first thing, while the implementation in FALSE returns the second. Here is what this might look like implemented in Java:
import java.util.function.Supplier;public interface Boolean {public <T> T ifThenElse(Supplier<T> ifTrue, Supplier<T> ifFalse);public void ifThenElse(Runnable ifTrue, Runnable ifFalse);}public class True implements Boolean {public <T> T ifThenElse(Supplier<T> ifTrue, Supplier<T> ifFalse) {return ifTrue.get();}public void ifThenElse(Runnable ifTrue, Runnable ifFalse) {ifTrue.run();}}public class False implements Boolean {public <T> T ifThenElse(Supplier<T> ifTrue, Supplier<T> ifFalse) {return ifFalse.get();}public void ifThenElse(Runnable ifTrue, Runnable ifFalse) {ifFalse.run();}}public class Demo {public static void main(String[] args) {Boolean b1 = new True();Boolean b2 = new False();int result = b1.ifThenElse(() -> 42, () -> 37 / 0);b2.ifThenElse(() -> System.out.println("Boo!"), () -> System.out.println("The answer is " + result));}}
The Supplier<T>
and Runnable
types are used because we want to pass in thunks to the ifThenElse
method.
A thunk is an anonymous function that takes no arguments; it is used to delay the execution of a block of code
until we are ready to get()
its value or run()
it. This is needed in Java (and in almost all common programming
languages except Haskell and Scala) because otherwise ifThenElse
would evaluate both of its arguments before performing
the method call; in the example, without using thunks, the call to b1.ifThenElse
would result in a division by zero
exception, and the call to b2.ifThenElse
would print Boo!
as well as the correct answer.4
Pairs and Lists
Inspired by the object-oriented view of the Boolean encoding, one way to represent a pair of values is to construct a function holding both values and expecting a "selector" that can retrieve either one of them on demand. Since the selector is just a function that chooses which of the two elements to retrieve, it can be a Boolean. This leads to the following encoding:
You should check that if , then reduces to 1, while reduces to 2.
Using pairs, we may build up lists with the following two helpers:
The list is encoded as . Given a list , returns TRUE if the list is empty and FALSE otherwise. If is non-empty, then extracts its head and its tail.
We may also use pairs to construct the promised function PRED.5 Consider the function . Then . If you iterate a non-zero number times and apply it to , then the result will be (prove this by induction on ). Therefore, we may define PRED as:
This has the disadvantage that computing the predecessor of takes time, but no one said the Church encodings were efficient.
Recursion
Consider the recursive definition of the factorial function in ReasonML:
The body of the function needs to have access to the definition of fact
itself; this is the point of the rec
qualifier with let
.
If we could provide the definition of fact
as another argument to the function, then we could express factorial with the following
"template":
This template describes how we could define fact
if we already had the fact
function: fact = fact_template(fact)
.
To break out of the circularity of this definition, what we really need is a way to solve the equation f = fact_template(f)
for f
; such a
solution is known as a fixed point of fact_template
.6
A remarkable fact about the lambda calculus is that there is a term that will find such a fixed point:
Observe what happens when we reduce :
That is, , so is a solution to the equation !
Using FIX, we may define the factorial function (and by this point you should be convinced that we may define any pure function that we could have written in ReasonML) as follows:
- The thesis was explicitly named in 1952 by Church's student Stephen Kleene, also known for introducing the star operator of regular expressions.↩
- There is also a notion of alpha () equivalence, which allows us to uniformly rename the bound parameter throughout a function, so that is the same function as . This is important for precise theoretical work, but we will be able to ignore the issue.↩
- Interestingly, the encoding for FALSE is the same (up to -equivalence) as the encoding of zero—apparently C made the right choice in equating zero and false!↩
- Side effects!↩
- This version of the predecessor function was discovered by Stephen Kleene, reportedly while visiting the dentist.↩
- Another way to view this is that each application of
fact_template
takes an imperfect approximation to factorial and produces a better approximation. Finding the fixed point is just taking the limit of this series of approximations.↩