λf.f (λa b c.a c (b c)) λd e.d
Lambda calculus is a formal system that creates a simple and universal model of computation.
A formal system is a precise collection of rules for manipulating symbols. These are usually organized into axioms, theorems and inference rules.
These systems are completely specified and don't rely on any external knowledge.
Published by Alonso Church in 1936 CE, it slightly predates Turing Machines. Both can compute any generally recursive function.
A brief overview (notation will be explained later)
[^λ.()\s]+
(E
)|
λv
.E | E E
This is everything in lambda calculus.
v :=
[^λ.()\s]+
A variable is a sequence of characters excluding
λ
, .
,
(
, )
and
white space. They serve as labels and focal points
for substitution.
Examples: a b c MULTIPLY 172 IS-NIL? @!
E :=
v |
(
E )
|
λ
v
.
E |
E
E
Application is conventionally left associative.
E
F
G
is the same as
(E
F)
G
but not
E
(F
G)
A free variable is one not bound by abstraction. This is how we determine which variables are free:
Every expression is associated with exactly one set of free variables, which may be empty.
Replaces free variable with expression: [a := G]
Replacing all occurances of a bound variable does not change the meaning of an expression as long as free variables are preserved.
b ∉ FV[E] ⟺ λa.E ≑ λb.(E [a := b])
E ≑ F ⟹ FV[E] = FV[F]
|
|
Reduction applies an abstration to the expression that follows. This replaces the abstraction and following expression with the body of the abstraction with the bound variable substituted.
(λa.E) F ... ⇝ E [a := F] ...
Free variables in the application argument must not match bound variables in nested abstractions.
(λa.b) b ⇝ b [a := b] ⇾ b
(λa.λb.a) b ⇝ λb.a [a := b] ⇾ λb.b
(λa.λc.a) b ⇝ λc.a [a := b] ⇾ λc.b
We must rename bound variables sometimes.
An expression is said to be in normal form if there are no reductions that can be performed.
The Church-Rosser theorem proves that if an expression has a normal form there is only one. When more than one reduction is possible the order will not result in different final forms.
Not all expressions have a normal from.
(λa.a a) λa.a a
⇝ a a [a := λa.a a]
⇾
(λa.a a) λa.a a
⇾ (λa.a a) λa.a a
⇾
(λa.a a) λa.a a
⇾ ...
This is a simple example, but there could be cycles or complex sequences that don't terminate.
Normal order performs the outer most, left most reduction first when more than one are available.
(λa.a a) ((λb.b) c)
⇝ a a [a := ((λb.b) c)]
⇾
((λb.b) c) ((λb.b) c)
⇝ b [b := c] ((λb.b) c)
⇾
c ((λb.b) c)
⇝ c (b [b := c])
⇾ c c
✓
Normal order is guaranteed to reach a normal form for any expression if one exists.
A disadvantage of normal form is that it can evaluate parts of an expression many times. Applicative evaluation solves this problem.
(λa.a a) ((λb.b) c)
⇝ (λa.a a) (b [b := c])
⇾
(λa.a a) c
⇝ a a [a := c]
⇾ c c
✓
Only two reductions were necessary.
Sometimes applicative order fails...
(λa.λb.a) c ((λd.d d) λd.d d)
|
|
Stable or efficient -- pick one!
Lazy evaluation is like normal order but it keeps of expressions so they're evaluated only once.
Stable, efficient... and tricky to implement.
Frege (1893 CE) pointed out that single argument functions can emulate multi-argument functions.
Supplying some but not all arguments is allowed.
Nested abstractions have a convenient short hand.
λa b.E ≝ λa.λb.E
λa b c.E ≝ λa.λb.λc.E
...
Spaces separate variables.
[^λ.()\s]+
(E
)|
λv
.E | E E
This is a JavaScript implementation.
Lambda calculus is certainly simple. But isn't it supposed to be universal?
Some things lambda calculus seems to lack:
We can construct these!
One thing we might want to compute is boolean logic. Everything must evaluate to true or false. How could we represent these in lambda calculus?
IF p THEN ELSE
We don't need a conditional operator.
NOT: λp a b.p b a
|
|
AND: λp q.p q p
|
|
|
|
OR: λp q.p p q
|
|
|
|
BOOLEQ?: λp q.p q (NOT q)
|
|
|
|
Church Numerals represent numbers with repeated application. Conceptually: n = λf a.fn a
Natural numbers consist of zero and its successors.
ZERO: λf a.a [FALSE]
SUCCESSOR: λn f a.f (n f a)
SUCCESSOR: λn f a.f (n f a)
|
|
And so on.
Take the successor repeatedly to add:
ADD: λm n.n SUCCESSOR m
Compose numbers to multiply:
MULTIPLY: λm n f.m (n f)
Raise one number to the power of another:
POWER: λn m.m n
Subtraction is addition reversed.
SUBTRACT: λm n.n PREDECESSOR m
PREDECESSOR: λn f a.n (λg h.h (g f)) (λc.a) λb.b
Zero has no predecessor but our operator reduces to zero. This isn't strictly correct but is useful.
Division is multiplication reversed.
DIVIDE:
λn.((λf.(λa.f (a a))
λa.f (a a))
λc n m f a.(λd.(λn.n
(λa.λa b.b) λa b.a) d a
(f (c d m f a)))
((λm n.n
(λn f a.n (λg h.h (g f))
(λc.a) λb.b) m) n m))
((λn f a.f (n f a)) n)
Any questions? Of course not. Moving on...
We can ask questions about numbers:
IS-ZERO?: λn.n (λa.FALSE) TRUE
|
|
What happens if we ask about a successor of one?
How can we ask whether a number is even?
IS-EVEN?: λn.n NOT TRUE
|
|
|
How could we define IS-ODD?
We can easily use a Church numeral directly to loop a fixed number of times.
However, we still don't have a way to stop a loop when some condition is met. That's an important gap in our programming ability.
These require statements, which we don't have. See recursion.
These require named functions, which we don't have. See loops.
Normal order fixed point (or Y) combinator:
FIX ≝ λf.(λa.f (a a)) λa.f (a a)
FIX g
⇾ (λa.g (a a))
λa.g (a a)
⇾
g ((λa.g (a a)) λa.g (a a))
≑ g (FIX g)
⇾
g (g (FIX g))
⇾ g (g (g (FIX g)))
⇾ ...
Forever? Normal order reduces outer most first.
FSTEP: (λf n.IS-ZERO? n ONE
(MULTIPLY n (f (PREDECESSOR n))))
FACTORIAL ≝ FIX FSTEP
FACTORIAL ONE ≝ FIX FSTEP ONE
Factorial | Result | Steps |
---|---|---|
0! | 1 | 9 |
1! | 1 | 30 |
2! | 2 | 127 |
3! | 6 | 646 |
4! | 24 | 3,873 |
5! | 120 | 26,899 |
... | ... | ... |
Let's revisit division:
DIVIDE: λn.FIX (λc n m f a.(λd.IS-ZERO? d a
(f (c d m f a))) (SUBTRACT n m)) (SUCCESSOR n)
This is just repeated subtraction.
We can take advantage of closure to store values.
This will serve as a foundation for data structures.
How does this work in practice?
Chaining ordered pairs together to make lists requires a way to find the end.
IS-NIL? NIL
NIL acts like a pair but ignores the function
IS-NIL? (PAIR FIRST LAST)
Pairs are not NIL regardless of contents
Now we can construct lists of arbitrary length.
So how do we work with these?
We can traverse a list using Church numerals.
NTH: λn l.n (λl.IS-NIL? l NIL (TAIL l)) l
An index beyond the end of the list will give NIL
How many elements are in our list?
Let's add up a list of numbers.
Almost too easy:
AVERAGE ≝ (λl.DIVIDE (SUM l) (COUNT l))
This will round down to the nearest natural number, which is not ideal.
We can apply a function to every member of a list
How can we construct an integer? Consider a pair of Church numerals. If we interpret them as a subtraction we can represent any integer value.
INT-CREATE: PAIR n m
This represents the integer n - m
Here are some simple tools for integers.
Integer arithmetic is messy but straightforward.
How can we represent rational numbers? Consider an pair of integers. If we interpret this as a division we can represent any rational number.
RATIONAL-CREATE: PAIR a b
This represents the rational number a / b
Here are some simple tools for rational numbers.
And of course we can add and multiply.
Lambda calculus simple, compact and universal.
The Starling and Kestral combinators are universal (Schönfinkel 1924 CE). Like lambda calculus and Turing machines they can compute any computable function.
A combinator is an abstraction that has no free variables. As a consequence they have no state or external interactions.
Raymond Smullyan wrote a book about combinatory logic in 1985 CE titled To Mock a Mockingbird. Giving names to combinators helps us to remember them.
The identity has many Starling and Kestral forms:
S K K ≑ S K S ≑ S K (anything) ≑ I
A common pattern reduces to the Kestral:
S (K K) I ≑ K
The Kite is the Kestral followed by Identity:
Mockingbird is easy to make with Starling and Ibis
M ≑ S I I
Bluebird is easy to make with Starling and Ibis
B ≑ S (K S) K
Any lambda term can be converted to S K:
These rules cover all cases (Source: Wikipedia)
Thrush: λa b.b a
Chris Barker 2001 CE: iota combinator is universal
i ≝ λf.f (λa b c.a c (b c)) λd e.d
Any combinator can be a bewildering set of iotas
Chris Barker created a simple binary encoding:
term := "1" | "0" term term
But remember... just because you can stick peas up your nose doesn't mean you should.
None of this is efficient compared to conventional von Neumann programming languages.
What's the point?
Formal systems can be easier to study than those
intended for practical use.
For example, some problems are not decidable
which means no algorithm can resolve them for all
possible inputs.
Example: the Halting Problem. No algorithm can determine whether any possible computation halts in a finite number of steps.
As we've seen, expressions are universal, stateless (therefore can be computed in parallel).
This makes them a powerful foundation for functional programming languages.
Running untrusted code requires limiting cycle consumption and memory. How would you do that with a conventional programming language?
It's obvious how to do this with lambda calculus: limit reductions and expression size.
Computation is powerful and can bloom from simple seeds.