The classic inductive definition of natural numbers is well known: zero is a natural number, and the successor of a natural number is a natural number. In a language that supports inductive types, it can be written somewhat like this:
data Nat :: * where
Zero :: Nat
Succ :: Nat -> Nat
zero :: Nat
zero = Zero
one :: Nat
one = Succ Zero
two :: Nat
two = Succ (Succ Zero)
add :: Nat -> Nat -> Nat
add n Zero = n
add n (Succ k) = Succ (add n k)
mul :: Nat -> Nat -> Nat
mul n Zero = Zero
mul n (Succ k) = add m (mul n k)
pred :: Nat -> Nat
pred Zero = Zero
pred (Succ k) = k
minus :: Nat -> Nat -> Nat
minus Zero m = Zero
minus n Zero = n
minus (Succ k) (Succ l) = minus k l
The simplest way to encode such numbers in the untyped lambda calculus is Church encoding, which seems to have no direct connection to the intuitive inductive type:
Here, exponent notation is used as syntactic sugar for the following definition:
Despite the strange appearance of these terms, we can find an analogy: z in the lambda terms represents zero, while a number of s terms preceding it represent incrementing that zero several times. To make the analogy stronger still, let us introduce a succ function:
Indeed, using this term produces the expected behaviour:
The simplest arithmetic operations are also available to us; they can be checked in exactly the same way as the succ function above:
These functions reveal an important property of Church-encoded natural numbers. Applying the term representing the th natural number to terms f and x causes f to be applied to x exactly times:
Subtraction and division are somewhat more difficult and use primitive recursion. To build them, we will need several auxiliary constructions. First, let us introduce two-element pairs. The inductive definition of a pair looks like this:
data Pair :: * -> * -> * where
MkPair :: a -> b -> Pair a b
fst :: Pair a b -> a
fst (Pair x y) = x
snd :: Pair a b -> b
snd (Pair x y) = y
Once again, however, we must encode them using the lambda calculus:
Indeed, any pair of elements can be written this way, because . Moreover, we have two projections that return the first and second elements of the pair:
Here, and are the combinators and , respectively. Indeed:
How can we use this construction to obtain the preceding number? Let us define a pair whose first argument is some number and whose second argument is its successor. It is easy to construct:
In particular, the first several pairs will be as follows, assuming that zero's predecessor is zero:
Instead of writing out every pair, can we simply invent a term that produces the next pair from the previous one? Easily!
The first element of the new pair is the second element of the previous pair, and the second is the next number. Applying this function to the pair exactly n times gives us a pair whose second element is n and whose first element is n - 1. That is precisely what we need. Putting everything together gives us the predecessor function for natural numbers:
The same reasoning gives us subtraction:
An important observation here is that pred takes time, while minus takes no less than ! This is an obvious problem with Church encoding for natural numbers.
Integer division can also be obtained by recalling its recursive expression in terms of subtraction:
To write this expression, we need one more data type: Boolean, which will express the condition according to which we either return zero or follow the recursive branch. The classic definition of Boolean is trivial: it is a type inhabited by two concrete values.
data Bool :: * where
True :: Bool
False :: Bool
ifThenElse :: Bool -> a -> a -> a
ifThenElse True x y = x
ifThenElse False x y = y
Its representation in the untyped lambda calculus is not particularly complicated either:
The if operator for this encoding is entirely trivial:
We can verify this directly:
To obtain our division formula, we must also work out how to compare two numbers. The simplest method is to subtract one from the other and compare the result with zero. We already know how to subtract, but comparison with zero is a new point of contact between two worlds: natural numbers and Boolean values. Nevertheless, the comparison is easy to implement. Observe that zero looks exactly like false, while a non-zero number applies some function to zero n times. This allows us to write an iszero function by analogy with if:
We can now write the recursive form of division:
Excellent! With one exception: we do not yet know how to work with recursive terms. But that is easy to fix. First, let us abstract over the occurrence of div in our term:
Our term now has the form . In other words, div is a fixed point of the term:
According to the fixed-point combinator theorem, every term has such a point, which can be found simply by applying a fixed-point combinator to the term:
The final non-recursive expression for division is therefore:
Let us summarise. We have seen simple and intuitive definitions of data types such as natural numbers, pairs, and Boolean values. We have also defined a set of functions for working with each of them. The next step was the seemingly magical appearance of lambda terms that reproduce the behaviour of these constructions. Combining them with primitive recursion and expressing recursive terms through the Y combinator gave us the same set of functions. The question we will try to answer next time is this: is there an algorithm that translates intuitive inductive constructions into lambda terms?