Lambda
Lambda
Helmut Brandl
Abstract
An introduction into lambda calculus emphasizing the use of lambda
calculus as a programming language.
Contents
Motivation 1
Arithmetic 11
Church Numerals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
Simple Arithmetics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
Simple Predicates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
Recursion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
Searching with Predicates . . . . . . . . . . . . . . . . . . . . . . . . . 16
Unbounded Search . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
Data Types 22
Construction Principle . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
Church Numerals Revisited . . . . . . . . . . . . . . . . . . . . . . . . 25
Lists . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Trees . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
References 30
Motivation
Lambda calculus is a fascinating topic for the following reasons.
1. It is simple. It just consists of variables, functions and function applications.
1
2. Despite of being simple it is possible to express any computable function
in the calculus.
This is the reason why Alonzo Church (1936) invented the lambda calculus.
He wanted to explore the limits of computability and decidability. If you
want to prove that something is undecidable you need a clear definition
what you mean by computable or decidable. Lambda calculus is the proper
tool.
3. It is fun. In lambda calculus we don’t care about execution. We just
express functions. And as you will see: We can express arbitrarily complex
and interesting functions. No matter if their execution would last longer
than the universe exists.
4. We can learn a lot about computation. In lambda calculus we can express
iteration, recursion, arbitrary data structures by using only variables,
functions and function applications.
In the following I do not expect any prior knowledge. The reader should have
some experience with programming.
All concepts are introduced step by step.
Many texts on lambda calculus use a lot of math. The goal usually is not to do
programming in lambda calculus, only to demonstrate its computational power.
In this text we use lambda calculus as a programming language. We build first
simple functions and step by step compose the simple functions to more complex
functions. At the end, the same goal is achieved: Demonstrate the expressive
power of lambda calculus. But I hope that leaving out math notation makes the
topic more accessible.
2
We pronounce the term \ x := e lambda x body e. The backslash should remind
us of the greek letter λ.
The most interesting term is the function term.
\ x := term
^ ^-- body (might contain variable x)
|
\------ bound variable
The bound variable is meaningful only within the body. We can change the name
of the bound variable arbitrarily as long as we change it consistently within the
body.
What can we do with a function? We can apply it to an argument and get (\
x := e) a. This term is called a reducible expression (short redex). As the
name says, this expression can be reduced or we can compute the result of the
function applied to the argument.
(\x := e) a ~> e[x:=a]
where ~> reads reduces to and e[x:=a] is the expression e where all occurrences
of the variable x have been replaced by a. A reduction is the most elementary
computation step in lambda calculus.
We pronounce the substitution e[x:=a] e with a for x.
Example: Application of the identity function
(\x := x) a ~> x[x:=a] = a
Conventions
There are some conventions which make writing lambda terms more convenient.
Function application associates to the left
f a b c = ((f a) b) c
Nested function abstractions associate to the right
\ x := \y := e = \x := (\y := e)
and the arguments of nested function abstractions can be compressed.
\ x := \y := e = \ x y := e
3
\ x y := x = \ z u := z
\ x y := y = \ z u := u
\ x x := x = \ x y := y
This is the same as with any other programming language. The names of formal
arguments of a function are irrelevant to the caller of the function.
Furthermore we have to make sure to choose names for bound variables which
cannot change the meaning of an expression.
The last example demonstrates that a variable is always bound by the innermost
binder.
Substitution
Since the basic computation step is based on substitution, we better define
substitution exactly.
x[x:=e] = e -- same variable
Substitution Example
(\ x y := x) a b
= (\x := (\ y := x)) a b
= (\ y := a) b
4
~> a[y:=b]
= a
The hygiene condition tells us that y must not occur in a (otherwise we would
have to rename y).
So we get
(\ x y := x) a b ~> a
i.e. the term \x y := x applied to two arguments returns the first argument
and ignores the second argument.
In the same manner
(\ x y := y) a b ~> b
can be shown.
Combinators
Lambda terms where all variables are bound are called combinators.
Projections
We have seen already the combinators \ x y := x and \x y := y. In order to
use them later on we give them names.
K := \ x y := x
KI := \ x y := y
We have chosen the names K and KI to be inline with the literature on lambda
calculus which uses the names K combinator and KI combinator.
These two definitions give just names to the coresponding lambda terms. The
names are only for humans. The lambda calculus does not know of any names
which we give to combinators. I.e. whenever we see K in a lambda term we
always mean the term \ x y := x.
In order to write the definition of K and KI more like function definitions in a
programming language we use the syntax
K x y := x
KI x y := y
which we pronounce K x y with body x. We just replaced the backslash by a
name. Therefore we call \ x y := x an anonymous or unnamed function and K
x y := x a named function.
5
But note that we always mean the same thing. K x y := x is the same as K :=
\ x y := x. The first form just uses syntactic sugar.
= (\ x y := x) a
= (\ x := (\ y := x)) a
~> (\ y := x)[x:=a]
Boolean
Lambda calculus does not have any primitive values. It only has functions —
nothing else.
We have to find a way to express boolean values as functions.
Since there are only two boolean values, we can encode booleans as functions
taking two arguments. The boolean value true returns the first argument and
the boolean value false returns the second argument.
I.e. booleans are decisions which decide between two alternatives where the
alternatives are represented the by the two function arguments.
true x y := x
false x y := y
6
Note that true and K and false and KI are defined by the same lambda term.
We use different names to express different intentions. But again: This is just
for us. From the viewpoint of the lambda calculus the terms are equivalent.
It is not too difficult to define boolean functions. Negation can be defined as
not b := b false true
The correctness of the definition can be shown by the following reductions.
not true
not false
Pair
A lambda term representing a pair of values has to store in some sense the values.
We have already seen that lambda calculus is able to store one value. Remember
the term K a which stores the value a and returns it if another argument is given
to the term.
The right choice to represent a pair is a lambda term which expects three
arguments. The first two arguments are the terms which form the pair of values.
The third argument is a function using these two values as arguments.
(,) x y := \ f := f x y
Here we use the comma operator to use the notation (a,b) to express the pair
of a and b. We could have chosen e.g. the name pair and written pair a b
instead of (a,b).
The term (a,b) is a partial application. It expects a further argument which
should be a function taking two arguments.
We can use the combinators K and KI to extract either the first or the second
component of the pair.
7
first p := p K
second p := p KI
Let’s see this in action on the term first (a,b).
first (a,b)
= (\ x y f := f x y) a b K -- definition of '(,)'
~> K a b
~> a
Type Annotations
Untyped lambda calculus does not know of any types. Therefore the name.
However the programmer thinks in types. We have already talked about booleans
and pairs. These are types. We use types to express our intentions.
Since we want to do programming in lambda calculus, we want to be able to
express our intentions in the source code.
Example: Boolean
We have already seen, that a boolean value is represented as a choice between
the two arguments which follow. Usually our intention is to express a choice
between two things of the same kind, and not a choice between a string and a
number.
The lambda calculus does not care what arguments you provide. But we can
use types to express our intentions.
true (x: A) (y: A): A := x
8
Since these are just annotations which are ignored by the compiler, we don’t
need formal rules. Otherwise we would have to switch to typed lambda calculus
which we won’t do here.
Remember: Type annotations are comments.
Example: Pair
If (a,b) is a pair of values, then the types of a and b can be different. However
the type of the third argument, the function which uses the two values has to be
consistent with the component types.
We can express the constraint in the following way.
(,) (x: A) (y: B): (A -> B -> C) -> C
:=
\ f := f x y
The type of (,) is then
(,): A -> B -> (A -> B -> C) -> C
Since we are not formal on type annotations we can use Pair A B to express the
type of a pair where the first component is of type A and the second component
is of type B.
We can add type annotations to the terms K, KI, first and second as well.
K (x: A) (y: B): A := x
9
Normal Forms
We call a lambda term to be in normal form if it contains no more redexes.
A term not in normal form can contain one or more redexes. A computation
step is done by choosing any redex and reduce it. Therefore evaluating a lambda
term might be non deterministic.
A reduction sequence might lead to a term in normal form or be an infinite
sequence.
t1 ~> t2 ~> t3 ~> ....... ~> tN -- 'tN' is in normal form
Loops
We can apply a lambda term to itself. This is the easiest form to build an infinite
loop.
M x := x x
Let’s see what happens if we apply M to itself.
M M
= (\ x := x x) M
~> (x x)[x:=M]
~> M M
~> M M
...
I.e. the term M M is neither strongly nor weakly normalizing. It is diverging.
The term M M is not useful at all. It is just a silly term to demonstrate what can
go wrong, because an infinite loop is certainly not desirable.
But we can construct another term which is only potentially non terminating.
U x f := f (x x f)
Let’s see what happens if we apply U U to an arbitrary argument.
10
U U g
= (\ x f := f (x x f)) U g
~> g (U U g)
~> g (g (U U g))
~> g (g (g (U U g)))
...
Here we get a potentially infinite reduction sequence. But we can choose g in
a manner that it might in some states ignore its argument i.e. which might
generate some exit condition from the loop.
In this introductory chapter we don’t look deeper into this possibility. We just
wanted to demonstrate that we can program loops in lambda calculus which are
potentially infinite.
Arithmetic
Church Numerals
We want to encode the natural numbers 0, 1, 2, ... in lambda calculus.
Since lambda calculus only has functions, we have to figure out a way to encode
numbers as functions.
What can we do with a number n? We can do something n times. And what
can lambda calculus do? Correct answer: Function application.
Therefore we encode a natural number as a function which takes a function
argument and a start value and iterates the function n times on the start value.
This is called the Church encoding of numbers and the encoded numbers are
called Church numerals.
So we encode the number zero as
zero f s := s
The successor function just takes a church numeral and applies the function one
more time.
successor n :=
\ f s := f (n f s)
We can use the combinators zero and successor to generate arbitrary church
numerals.
11
one := successor zero
...
Let’s check, if the definition really behaves as expected.
one
Simple Arithmetics
Church numerals are iterations. We can use that to do simple arithmetics. To
add the numbers n and m which are represented as church numerals we simply
apply the successor function n times with start value m.
(+) n m :=
n successor m
Multiplication of the numbers n and m is defined as n times the iterated addition
of m on the number zero.
(*) n m :=
n ((+) m) zero
The exponentiation n ˆ m is defined as n * n * .... * n * one, i.e. it is an
m times iterated multiplication. There is no problem to define exponentiation in
lambda calculus
(^) n m :=
m ((*) n) one
12
Simple Predicates
A predicate is a function returing a boolean value. Predicates are deciders. We
want to be able to decide, if a number is zero, is an even number or is an odd
number.
The encoding of the predicate isZero as an iteration is surprisingly simple.
Evidently the start value of the iteration is true, because the number zero is
zero. The iteration function just ignores the result of the previous iterations and
returns false.
isZero n :=
n (\ _ := false) true
The evenness and oddness predicates can be represented as iterations as well.
The start value for isEven is true and the start value for isOdd is false to return
the correct value for the number zero.
On each iteration step we toggle the truth value of the result by using the
function not.
isEven n :=
n not true
isOdd n :=
n not false
Recursion
Up to now all functions on church numerals have used iterations. This technique
has its limits. Assume you want to write the predecessor function which returns
zero for zero (since zero has no predecessor) and the actual predecessor for any
other number. Trying iteration our function looks like
predecessor n :=
n (\ x := ?) zero
The start value is clear. According to the definition it has to be zero. But
how to design the step function? The task of the step function is to map the
predecessor of the predecessor into the predecessor of the current number. In
nearly all cases adding one does the job. But it fails for the number one, since
the predecesssor of zero is zero we would compute one as the predecessor of
one which is wrong.
Alonzo Church, the inventor of the lambda calculus, had been puzzled to find
a proper definition of the predecessor function. A difficult situation when you
want to define a calculus where all computable functions can be encoded and the
calculus fails on such a simple task as to compute the predecessor of a natural
number.
13
One of his phd students, the mathematician Stephen Kleene (pronounced Klay-
nee), came up with a solution which not only let us encode the predecessor
function, but also a lot of other complex functions.
The problem with iteration: The step function has only access to the function
result of the function called on the predecessor argument, but not to the value
of the predecessor itself. The iteration consumes the numbers. This is clear if
we look at the type signatures of the start value s and the iteration function f.
s: A
f: A -> A
s has a value of some type A and the iteration function f maps step by step the
value into its final result value. What we want is the following types:
s: A
14
:=
second (n step (zero, s)) where
step p :=
p (\ i res :=
(i + one, f i res))
Note how the step function has two components. The first component just
increments the iteration counter and the second component uses the function f
to compute from the iteration counter and the result of the previous iteration
the result of the current iteration.
Having such a generic recursor, the encoding of the predecessor function is just
a piece of cake.
predecessor n :=
nat-rec n (\ i _ := i) zero
predezessor zero is zero because of the start value zero. predecessor one
is zero because the iteration counter is zero. predecessor n receives at the
last iteration the iteration counter n - 1 which is exactly the required result.
The iteration function f just used the iteration counter to compute the next
result and ignores the result of the previous iteration.
Based on the function predecessor we can encode the difference between two
natural numbers
(-) n m :=
m predeccessor n
Coding of comparison functions is now easy.
(<=) n m :=
isZero (n - m)
(<) n m :=
successor n <= m
equal n m :=
n <= m and m <= n
Even the factorial function computing 1 × 2 × 3 × ... × n is now possible which
uses the recursive definition
0! = 1
n! = n ∗ (n − 1)! for 0 < n
factorial n :=
nat-rec
n
(\ i res := res * (i + one))
one
15
It might not be immediately obvious that factorial really does the right thing.
In order to be sure, the following table shows the computation steps for different
arguments.
argument computation
0 1
1 1*1
2 1*1*2
3 1*1*2*3
16
The expression p res checks if res satisfies the predicate. If the answer is true,
then the value res is kept for the next iteration. If the answer is false, then
the value of res is incremented. If no number below n satisfies the predicate p,
then the value zero is incremented n times, i.e. n is returned as the final result.
Next we want to implement an existential quantifier with an upper bound.
If we find a number below the bound which satisfies a certain predicate, we know
that at least one number below the bound exists, which satisfies the predicate.
The existential quantifier with an upper bound can be implemented by looking
at the result of least-below and comparing it with the bound.
exist-below (n: Natural) (p: Natural -> Boolean): Boolean :=
-- Is there a number 'i' below 'n' satisfying 'p i'?
least-below n p < n
If there exists no number below a bound which does not satisfy a certain
predicate, then all numbers below the bound satisfy the predicate. Therefore
implementation of the universal quantifier with an upper bound is easy as well.
all-below (n: Natural) (p: Natural -> Boolean): Boolean :=
-- Are all numbers 'i' below 'n' satsifying 'p i'?
not
(exist-below
n
(\ x := not (p x)))
Division
With these helper functions based on predicates we can implement division
functions and functions computing prime numbers.
The value of a divided by b i.e. a / b is the unique solution x of the inequalities
b * x <= a
a < b * (x + 1)
Division by zero is undefined. In the case b = 0 the second inequality is cannot
be satisfied. In that case we want the expression a / b to return a in order to
have a total function.
We can use least-below with upper bound a to find the smallest number x
which satisfies the second inequality. In case that no such numbers exist, we
get as expected the upper bound a. But are we sure that the first inequality is
satisfied?
From the reasoning above we know, that the function least-below maintains
the invariant for all numbers strictly below x
not (a < b * (x + 1))
17
which is equivalent to
b * (x + 1) <= a
and
b * x + b <= a
which in turn implies
b * x <= a
Therefore the first inequality is maintained by the function least-below.
I.e. the following implementation is correct.
(/) a b :=
least-below
a
(\ x :=
a < b * (x + one))
The function divides a b shall decide, if a divides b exactly i.e. if there exist a
solution x satisfying
x * a = b
The number b / a is a good candidate for the solution x, because according to
its definition it satisfies
b / a * a <= b
So we just compute b / a * a and compare it with b.
divides a b :=
equal (b / a * a) b
Prime Numbers
Prime numbers are very important in number theory and cryptography. In this
section we show the implementation of some important prime number functions.
A prime number is a natural number greater than 1 which is only divisible by 1
and itself.
If we reformulate the definition a little bit, we can implement it and get a prime
number tester in lambda calculus.
isPrime n :=
one < n
and
all-below
n
(\ x :=
18
x <= one
or
not (divides x n))
If we want to compute the nth prime number, we have to think a little bit.
We know that two is the first prime number.
If we have the ith prime number pi, we get the next prime number by finding
the smallest number x strictly above pi which satisfies isPrime x. In order to
use the function least-below we need an upper bound for the search.
Let’s find an upper bound for the next prime number above pi. We form the
product z = p0 * p1 * ... * pi of all prime numbers below pi including pi.
Certainly none of these prime numbers divides z + 1 because each division
leaves the remainder 1. Therefore z + 1 is either a prime number or there exists
a prime number different from the prime numbers in the product dividing z +
1. Therefore z + 2 is a strict upper bound for the next prime number.
Next we observe that z <= factorial pi is valid, because the factorial is the
product of more numbers than z. Therefore factorial pi + 2 is a strict upper
bound for the next prime number above pi.
Now the implementation is straightforward.
nth-prime n :=
n f two where
f p_i :=
-- p_i is the 'i'th prime
least-below
(factorial p_i + two)
(\ x := p_i < x and isPrime x)
Note that the above reasoning to find an upper bound for the next
prime number is the reasoning which has been used by Euclid to
prove that there are infinitely many prime numbers.
From number theory we know that every natural number above zero has a unique
prime number factorisation. I.e. each positive number n can be written as the
infinite product
n = p0 ^ e0 * p1 ^ e1 * p2 ^ e2 * ...
where pi is the ith prime number and ei is the corresponding exponent. For n
= 1 all exponents are 0.
We want to have a function which computes for all numbers n the exponent ei
of the ith prime number.
For each pair (pi,ei)
divides (pi ^ k) n
19
is valid for all k <= ei and
divides (pi ^ (ei + 1)) n
is invalid.
Therefore we can find the exponent by a search for the least number which does
not satisfy the last proposition.
The upper bound for the search is easy to find. Since all prime numbers are
greater than 1, all exponents are lower than n.
prime-exponent i n :=
-- Exponent of the 'i'th prime number in 'n'
least-below
n
(\ x :=
not (divides
((nth-prime i) ˆ (x + one))
n))
The prime factorization for the number 0 is not defined. However the func-
tion prime-exponent i zero returns zero. This is no problem. We have just
assigned an arbitrary result to the function for arguments, where it is mathe-
matically undefined. For all other arguments the function returns the correct
exponent.
Unbounded Search
If we have a predicate p: Natural -> Boolean and know that there exists a
number which satifies the predicate, then we can find the least number by an
unbounded search. In traditional programming languages we would use a while-
loop which has the continuation condition not (p i) and which increments
in the body of the loop the number by one until the continuation condition is
violated.
In lambda calculus we don’t have while loops. Therefore we have to find a way
to do the search with functions.
We would like to write the function in the following form
search-least (p: Natural -> Boolean): Natural :=
iterate step zero where
step := ?
iterate := ?
which iterates a step function as long as needed starting with zero and main-
taining the invariant, that all numbers below the current number do not satisfy
the predicate.
20
The function step needs as an argument the current number to check. We could
try the following.
step i :=
p i i ?
If the term p i returns true, then p i i ? returns the value i which satisfies
the predicate. But we don’t know what to return in case that i does not satisfy
the predicate. If we had recursive functions in lambda calculus we would just
replace the question mark with step (i + one). Unfortunately this is not a
valid lambda term.
But we can give the step function another argument, which is a continuation
(traditionally called k) knowing how to do the rest of the computation.
step k i :=
p i i (k (i + one))
Now the rest of the difficulty remains on the unkwnown term iterate. We just
know that this term has to do some kind of self replication to implement the
loop. In the chapter Basics of Lambda Caluculus we have already encountered a
combinator U which does some kind of replication.
U x y := y (x x y)
The combinator U expects two arguments and returns a term which contains
both arguments twice. It is interesting to see what happens, if we evaluate U U
step i. We get
U U step i ~> step (U U step) i
I.e. U U step i calls step with U U step as first argument and i as second
argument. Now step is in the function position and has control of what to do
next. The function step evaluates p i. If the result is true then it returns
i and the iteration terminates. If p i evaluates to false then it return (U U
step) (i + one) and the iteration can continue.
The iteration is started with U U step zero i.e. we can use U U as iterate and
we are ready.
We see, that we have implemented an iteration which stops, as soon as a number
is encountered which satisfies the predicate. The complete function reads
search-least (p: Natural -> Boolean): Natural :=
U U step zero where
step k i :=
-- invariant: all numbers below `i` do not
-- satisfy `p i`.
p i i (k (i + one)
U x y :=
y (x x y)
21
It might be necessary to read this section twice or more to understand the tricky
mechanism to implement the unbounded search. But it is possible.
However I admire the genious, who invented it. I would have never found such a
cleverly constructed lambda term by myself.
Some remarks:
• All functions constructed before this section on unbounded search are
strongly normalizing (provided that their arguments are of the proper
kind). The function search-least is only weakly normalizing (provided
that a number exists which satisfies the predicate, otherwise it is diverging).
I.e. there are only some reduction sequences, which terminate with the
desired result. But there are other reductions sequences which are infinite.
The subterm U U step has an infinite reduction sequence, because it
reduces to step (U U step) which contains itself as a subterm.
However there are reduction strategies, which find for all weakly normalizing
terms a reduction path which terminates.
• As long as you remain in constructive mathematics, you don’t need un-
bounded search. Unbounded search needs a guarantee, that a number
satisfying the predicate exists. In constructive mathematics an existence
proof requires a construction of an object which satisfies the condition.
But if you have a construction of such an object, you can use it as an
upper bound and use least-below to find the smallest number satisfying
the predicate.
• The availability of unbounded search makes lambda calculus as expressive
as general recursive functions. The class of general recursive functions
consists of the constant zero, the successor function, all projections (K
and KI cover the special case with two arguments, but the generalization
to more argumentes is obvious) and are closed under primitive recursion
(nat-rec) and minimization (unbounded search).
There are many definitions of computable functions. E.g.
– Recursive functions
– Turing machines
– Lambda calculus
Fortunately it can be proved that they are all equivalent i.e. they define
the same class of functions.
Data Types
Data types are an important means to structure computations. We think of
data types like Boolean, Natural, List, Tree, etc. In the previous chapters
22
represented the types Boolean and Natural in lambda calculus.
Since lambda calculus has only functions, we have to represent objects of a
certain type as functions. We represented the type Boolean as a function taking
two arguments and returning one of the them depending on its boolean value.
We represented natural numbers as functions taking two arguments. The first
argument is a function and the second a start value. A natural number iterates
the function n times on the start value.
It is possible to define any datatype in lambda calculus. In textbooks on lambda
calculus you are many times shown that it is possible e.g. to represent lists. It
sometimes look like some rabbit has been pulled out of the hat by some magic.
But no magic and no genious is needed to find lambda representations of
datatypes. There is a construction principle which is fairly general. In this
chapter we present the construction principle and show it on some old (natural
numbers) and new (lists and trees) examples.
Construction Principle
In order to understand the construction principle, we have to understand a
seemingly unrelated topic: Algebra.
An important example of an algebra is the concept of a group. A group must
have
• a set of elements
• a unit element (i.e. a constant)
• a unary function which applied to an element returns its inverse
• a binary function which let us combine elements
Apart from that it takes some properties. The unit element must be neutral
with respect to the binary operation. An element combined with its inverse is
the unit element. The binary operation is associative. However we don’t need
the properties here. The operations are sufficient.
In a functional language you could define a datatype like
class Group A :=
el: A -> Group A
unit: Group A
inv: Group A -> Group A
(<*>): Group A -> Group A -> Group A
with three constructors for the constant, the unary and the binary operation,
and one constructor (el) to produce elements from some generator set A.
We can use such a type to form expressions and each expression has some tree
associated with it.
-- expression
23
inv (el 1 <*> unit) <*> el 0
-- expression tree
<*>
|
---------------------
| |
inv el 0
|
<*>
---------
| |
el 1 unit
The numbers are just a silly example for an arbitrary generator set. Usually the
set of generators is finite.
But anyhow. The possible expressions don’t have any intrinsic meaning. Speaking
in terms of abstract algebra we have defined a signature. A signature is a set
of operation symbols where each operation symbol has an arity. An operation
symbol with arity 0 is a constant.
The signature just defines a collection of wellformed expressions in that algebra.
In order to give a meaning (i.e. semantics) to the expressions, we have to define
a function which evaluates the expression to a value of some type R (standing
for the result type).
This can be done by assigning a meaning to each operator symbol. For program-
ming this means that every symbol has to get type signature.
Note that Group A has been replaced by R i.e. each group expression corrensponds
to some value in its interpretation.
A lambda term representing some group expression is a function with four
arguments, one for each operation symbol. Each argument is a function (or
constant) according to the type signature. I.e. a lambda term representing a
group expression is an evaluator of that group expression.
It is straightforward to define the four constructors in lambda calculus.
el (a: A): Group A :=
24
\ e u i m := e a
unit: Group A :=
\ e u i m :=
u
-- expression tree
successor
|
successor
|
zero
25
A lambda term representing an expression of the algebra must have the type
(R -> R) -> R -> R
We get the following lambda terms for the constructors
zero: Natural :=
\ f s := s
Lists
Now let’s apply the construction pattern to lists and look at a definition of the
list type in a functional programming language.
class List A :=
cons: A -> List A -> List A
nil: List A
We interpret the type definition as the definition of an algebra and look at the
expressions it generates and at corresponding expressions trees.
-- expression
cons 0 (cons 1 (cons 2 nil))
-- expression tree
cons
|
---------
| |
0 cons
|
--------
| |
1 cons
|
-------
| |
2 nil
The definition has two symbols cons and nil with the following type signatures.
26
Therefore a lambda term of type List A must have the type
(A -> R -> R) -> R -> R
We apply the construction principle and get the lambda terms for the construc-
tors.
nil: List A :=
\ f s := s
27
list-rec (list: List A) (f: A -> List A -> R -> R) (s: R): R
:=
second (list step start) where
start :=
(nil, s)
step a p :=
p (\ tail res :=
(cons a tail, f a tail res))
Now we can define the function tail.
tail (list: List A): List A :=
list-rec
list
(\ head tail res := tail)
nil
In the same manner we can define a function head. In that case we have to
provide a default value since we cannot pull out an arbitrary list element from
an empty list.
head (list: List A) (default: A): A :=
list-rec
list
(\ hd tl res := hd)
default
Trees
I hope that the construction principle becomes clearer and clearer. As a last
expample we show how to represent binary trees as lambda terms.
In order to keep things simple we look at binary trees which store information
only in the leaves.
A type definition of such a tree in a functional programming language looks like
class Tree A :=
node: Tree A -> Tree A -> Tree A
leaf: A -> Tree A
As before we look at the expressions and expression trees of the corresponding
algebra.
-- expression
node (node (leaf 0) (leaf 1)) (leaf 2)
-- expression tree
node
|
28
--------------
| |
node leaf 2
|
-----------
| |
leaf 0 leaf 1
A lambda term representing a tree has two arguments one representing the
binary operation on the node and one which maps the leaf information into the
result type. I.e. the type Tree A is represented by a lambda term having the
type
(R -> R -> R) -> (A -> R) -> R
The lambda terms representing the two constructors look like.
leaf a :=
\ nd lf :=
lf a
node a b
\ nd lf :=
nd (a nd lf) (b nd lf)
With these constructors we can form tree expressions like node (node (leaf
0) (leaf 1)) leaf 2.
Some simple functions on trees:
count-nodes tree :=
-- Count the nodes in a tree including the leaf nodes.
tree
(\ size-a size-b := one + size-a + size-b)
(\ _ := one)
flip tree :=
-- Make a mirror image of the tree.
tree
(\ left right := node right left)
leaf
29
to-list (tree: Tree A): List A :=
-- Transform the tree into a list of leaf values
tree
concat
(\ el := cons el nil)
Exercise: Define a binary tree in lambda calculus where all information is stored
in the nodes and the leaves are empty. Implement the functions count-nodes,
flip and to-list for this kind of tree.
I hope that by looking at these examples it should be easy to represent any data
type which can be represented in a functional language.
This ends this introduction into programming with lambda calculus. I hope you
enjoyed it.
References
Church, Alonzo. 1936. “An Unsolvable Problem of Elementary Number Theory.”
Journal of Mathematics 58: 354–63.
30