Lecture 18 (2017-03-22)
Lambda calculus: encodings
After [defining the lambda calculus](Lec14.html), I made the claim that the lambda
calculus is a universal model of computation, just like Turing
machines and GĂ¶del numbers. To back up this claim, I started defining
the various elements we might expect in a programming language.
The key idea behind *all* of the encodings is to operationalize
things: since the lambda calculus only has functions, there is no
"data" per se.
#### Booleans

Booleans are functions that make a binary choice. Arbitrarily, we'll
say `true` takes the first of two choices.
```pre
true = lambda a b. a
false = lambda a b. b
not = lambda bl. lambda a b. bl b a
and = lambda b1 b2. b1 b2 false
or = lambda b1 b2. b1 true b2
```
To convince yourself that these definitions are correct, try to derive
the following equalities. (Again, try to mark when you use alpha and
when you use beta.)
```pre
not true = false
not (not true) = true
or false false = false
```
Once we have these booleans, we can encode conditionals as follows: to
write `if e1 then e2 else e3`, simple write `e1 e2 e3`. If `e1` is
true, we'll get `e2` back; if it's false, we'll get `e3`---just like a
conditional!
#### Naturals

The booleans were easy: there are only two possibilities. Naturals
presented a greater difficulty, since there are infinitely many. The
idea here is to think back to the idea of natural numbers from number
theory, where a natural number is either (a) zero, or (b) the
successor of some other natural number. We could do it in Haskell
easily:
> data Nat = Zero | Succ Nat
We can't explicitly represent such data structures in the lambda
calculus, though, since there's only functions, no data. But we can
operationalize the idea: the natural number `n` can be represented as
a function of two arguments: we apply the first argument `n` times to
the second. Natural numbers represented this way are called *Church
numerals*, in honor of Alonzo Church.
```pre
zero = lambda s z. z
one = lambda s z. s z
two = lambda s z. s (s z)
three = lambda s z. s (s (s z))
four = lambda s z. s (s (s (s z)))
```
While we can follow this pattern in principle to write down any
natural number---just apply the first argument, `s`, some number of
times---we can also define the successor function itself.
```pre
succ = lambda n. lambda s z. s (n s z)
```
Verify that `succ zero = one` and `succ (succ two) = four`.
We can go on to define other numeric operations, using the intuition
that `m + n = 1 + 1 + ... m times ... + n` and `m * n = n + n + ... m
times ... + 0` to define these operations.
```pre
plus = lambda m n. m succ n
times = lambda m n. m (plus n) zero
isZero = lambda n. n (lambda x. false) true
```
Verify that `times two two = four` and `isZero (times three zero) =
true`.
In order to encode the predecessor function, we had to make a detour
through pairs.
### Pairs

We defined pairs using what amounts to a "callback" strategy: a pair
is a function that takes a single callback, which it calls with the
first and second elements of the pair, respectively.
```pre
pair = lambda a b. lambda c. c a b
fst = lambda p. p (lambda f t. f)
snd = lambda p. p (lambda f t. t)
```
We can then observe that `fst (pair a b) = a` and `snd (pair a b) =
b`.
### Predecessor

The predecessor function is trickily partial: in the naturals, the
predecessor of zero is undefined. Our function will have to do
*something*, so let's just say it returns zero.
The trick to writing the predecessor function is similar to what we
did when we wrote folds that computed more than one value: we'll use
pairs to simultaneously compute our current number and one less than
that number.
```pre
pred = lambda n. snd (n (lambda p. pair (succ (fst p)) (fst p)) (pair zero zero))
```
How does `pred` work? Given a number, it does some computation on
pairs and takes the second element. To understand the computation,
let's break it into parts:
```pre
pred_zero = pair zero zero
pred_succ = lambda p. pair (succ (fst p)) (fst p)
pred = lambda n. snd (n pred_succ pred_zero)
```
So `pred n` is equal to `snd (pred_succ (pred_succ ... n times
... pred_zero))`. We can compute:
```pre
pred zero = snd pred_zero = zero
```
Which is just the behavior we specified. We can also see:
```pre
pred three = snd (pred_succ (pred_succ (pred_succ pred_zero)))
= snd (pred_succ (pred_succ (pred_succ (pair zero zero))))
= snd (pred_succ (pred_succ (pair one zero)))
= snd (pred_succ (pair two one))
= snd (pair three two)
= two
```
Note how, as we work through the number, the first element of the pair
is always our "current" number and the second is the predecessor of
our "current" number (except for when we're just starting at
zero). Try to prove to yourself that `pred (succ e) = e`.
Once we have predecessor, we can use the intuition that `m - n = m - 1
- 1 - ... n times ... -1` to define subtraction:
```pre
minus = lambda m n. n pred m
```
Once we have minus, we can define other standard predicates on the
naturals:
```pre
lte = lambda m n. isZero (minus m n)
gte = lambda m n. lte n m
lt = lambda m n. lte (succ m) n
gt = lambda m n. lt n m
equal = lambda m n. and (lte m n) (lte n m)
```