Basics of Lambda Calculus ========================= We often talk about "*the* Lambda Calculus", as if there were just one; but in fact, there are many, many variations. The one we will start with, and that we will explore in some detail, is the "pure" Lambda Calculus. And actually, there are many variations even in the pure Lambda Calculus. But all of the variations share a strong family resemblance, so what we learn now will apply to all of them. The lambda calculus we'll be focusing on for the first part of the course has no types. Some prefer to say it does have types, it's just that there's only one type, so that every expression is a member of that one type. If you say that, you have to say that functions from this type to this type also belong to this type. Which is weird... In fact, though, such types are studied, under the name "recursive types." More about these later in the seminar. Here is its syntax:
Variables:Each variable is an expression. For any expressions M and N and variable a, the following are also expressions:x
,y
,z
...
Abstract: (λa M)
We'll tend to write (λa M)
as just `(\a M)`, so we
don't have to write out the markup code for the
λ
. You can yourself write (λa
M)
or `(\a M)` or `(lambda a M)`.
Application: (M N)
Expressions in the lambda calculus are called "terms". Here is the
syntax of the lambda calculus given in the form of a context-free
grammar:
T --> ( T T )
T --> ( lambda Var T)
Var --> x
Var --> y
Var --> z
...
Very, very simple.
Examples of terms:
x
(y x)
(x x)
(\x y)
(\x x)
(\x (\y x))
(x (\x x))
((\x (x x)) (\x (x x)))
The lambda calculus has an associated proof theory. For now, we can regard the
proof theory as having just one rule, called the rule of **beta-reduction** or
"beta-contraction". Suppose you have some expression of the form:
((\a M) N)
This is an application whose first element is an abstract. This
compound form is called a **redex**, meaning it's a "beta-reducible
expression." `(\a M)` is called the **head** of the redex; `N` is
called the **argument**, and `M` is called the **body**.
The rule of beta-reduction permits a transition from that expression to the following:
M [a <-- N]
What this means is just `M`, with any *free occurrences* inside `M` of
the variable `a` replaced with the term `N`.
What is a free occurrence?
> An occurrence of a variable `a` is **bound** in T if T has the form `(\a N)`.
> If T has the form `(M N)`, any occurrences of `a` that are bound in `M` are also bound in T, and so too any occurrences of `a` that are bound in `N`.
> An occurrence of a variable is **free** if it's not bound.
For instance:
> T is defined to be `(x (\x (\y (x (y z)))))`
The first occurrence of `x` in T is free. The `\x` we won't regard as
containing an occurrence of `x`. The next occurrence of `x` occurs
within a form that begins with `\x`, so it is bound as well. The
occurrence of `y` is bound; and the occurrence of `z` is free.
Note that the definition of *bound* is carefully crafted to guarantee
that in an expression like `(\a (x a))`, both occurrences of `a` count
as bound.
To read further:
* [[!wikipedia Free variables and bound variables]]
Here's an example of beta-reduction:
((\x (y x)) z)
beta-reduces to:
(y z)
We'll write that like this:
((\x (y x)) z) ~~> (y z)
Different authors use different notations. Some authors use the term
"contraction" for a single reduction step, and reserve the term
"reduction" for the reflexive transitive closure of that, that is, for
zero or more reduction steps. Informally, it seems easiest to us to
say "reduction" for one or more reduction steps. So when we write:
M ~~> N
We'll mean that you can get from M to N by one or more reduction
steps.
There are many more details about the notation and the metatheory of
the lambda calculus here:
* [[topics/_week2_lambda_calculus_fine_points.mdwn]]
Shorthand
---------
The grammar we gave for the lambda calculus leads to some
verbosity. There are several informal conventions in widespread use,
which enable the language to be written more compactly. (If you like,
you could instead articulate a formal grammar which incorporates these
additional conventions. Instead of showing it to you, we'll leave it
as an exercise for those so inclined.)
**Parentheses** Outermost parentheses around applications can be dropped. Moreover, applications will associate to the left, so `M N P` will be understood as `((M N) P)`. Finally, you can drop parentheses around abstracts, but not when they're part of an application. So you can abbreviate:
(\x (x y))
as:
\x (x y)
but you should include the parentheses in:
(\x (x y)) z
and:
z (\x (x y))
**Dot notation** Dot means "put a left paren here, and put the right
paren as far the right as possible without creating unbalanced
parentheses". So:
\x (\y (x y))
can be abbreviated as:
\x (\y. x y)
and that as:
\x. \y. x y
This:
\x. \y. (x y) x
abbreviates:
\x (\y ((x y) x))
This on the other hand:
(\x. \y. (x y)) x
abbreviates:
((\x (\y (x y))) x)
**Merging lambdas** An expression of the form `(\x (\y M))`, or equivalently, `(\x. \y. M)`, can be abbreviated as:
(\x y. M)
Similarly, `(\x (\y (\z M)))` can be abbreviated as:
(\x y z. M)
Lambda terms represent functions
--------------------------------
Let's pause and consider the following fundamental question: what is a
function? One popular answer is that a function can be represented by
a set of ordered pairs. This set is called the **graph** of the
funtion. If the ordered pair `(a,b)` is a member of the graph if `f`,
that means that `f` maps the argument `a` to the value `b`. In
symbols, `f a == b`.
There is a requirement that in order to count as a function (rather
than as merely a more general relation), the graph cannot contain two
(distinct) ordered pairs with the same first element. That is, in
order to count as a proper function, each argument must correspond to
a unique result.
The essential usefullness of the lambda calculus is that it is
wonderfully suited to representing functions. In fact, the untyped
lambda calculus is Turing Complete [[!wikipedia Turing Completeness]]:
all (recursively computable) functions can be represented by lambda
terms. (As we'll see, much of the fun will be in unpacking the word
"represented".)
For some lambda terms, it is easy to see what function they represent:
> `(\x x)` represents the identity function: given any argument `M`, this function
simply returns `M`: `((\x x) M) ~~> M`.
> `(\x (x x))` duplicates its argument:
`((\x (x x)) M) ~~> (M M)`
> `(\x (\y (y x)))` reorders its two arguments:
`(((\x (\y (y x))) M) N) ~~> (N M)`
> `(\x (\y x))` throws away its second argument:
`(((\x (\y x)) M) N) ~~> M`
and so on. In order to get an intuitive feel for the power of the
lambda calculus, note that duplicating, reordering, and deleting
elements is all that it takes to simulate the behavior of a general
word processing program. That means that if we had a big enough
lambda term, it could take a representation of *Emma* as input and
produce *Hamlet* as a result.
Some of these functions are so useful that we'll give them special
names. In particular, we'll call the identity function `(\x.x)`
**I**, and the function `(\x\y.x)` **K**.
It is easy to see that distinct lambda expressions can represent the same
function, considered as a mapping from input to outputs. Obviously:
(\x x)
and:
(\z z)
both represent the same function, the identity function. However, we said above that we would be regarding these expressions as synactically equivalent, so they aren't yet really examples of *distinct* lambda expressions representing a single function. However, all three of these are distinct lambda expressions:
(\y x. y x) (\z z)
(\x. (\z z) x)
(\z z)
yet when applied to any argument M, all of these will always return M. So they have the same extension. It's also true, though you may not yet be in a position to see, that no other function can differentiate between them when they're supplied as an argument to it. However, these expressions are all syntactically distinct.
The first two expressions are *convertible*: in particular the first
reduces to the second via a single instance of beta reduction. So they
can be regarded as proof-theoretically equivalent even though they're
not syntactically identical. However, the proof theory we've given so
far doesn't permit you to reduce the second expression to the
third. So these lambda expressions are non-equivalent.
To use a linguistic analogy, you can think of what we're calling
"proof theory" as a kind of syntactic transformation. That is, the
sentences *John saw Mary* and *Mary was seen by John* are not
syntactically identical, yet (on some theories) one can be derived
from the other. The key element in the analogy is that this kind of
syntactic derivation is supposed to preserve meaning, so that the two
sentence mean (roughly) the same thing.
There's an extension of the proof-theory we've presented so far which
does permit this further move. And in that extended proof theory, all
computable functions with the same extension do turn out to be
equivalent (convertible). However, at that point, we still wouldn't be
working with the traditional mathematical notion of a function as a
set of ordered pairs. One reason is that the fully general
mathematical notion permits many uncomputable functions, but the
lambda calculus is capable of expressing only computable functions. A
second reason is that the full mathematical notion prohibits
functions from applying to themselves, but in the lambda calculus,
it is permitted for a function to take itself as an argument (for
instance, there is nothing wrong with applying the identity function
to itself, thus: `(\x x)(\x x)`. This is a redex that reduces to the
identity function (of course).
The analogy with `let`
----------------------
In our basic functional programming language, we used `let`
expressions to assign values to variables. For instance,
let x match 2
in (x, x)
evaluates to the ordered pair (2, 2). It may be helpful to think of
a redex in the lambda calculus as a particular sort of `let`
construction.
((\x M) N) is analogous to
let x match N
in M
This analogy should be treated with caution. For one thing, our
`letrec` allowed us to define recursive functions in which the name of
the function appeared within the expression `N`; we're not ready to
deal with recursive functions in the lambda calculus yet. For
another, we defined `let` in terms of values, and at this point, the
lambda calculus doesn't have values, it only has other expressions.
So it might be better to translate `((\x M) N)` as `let x be replaced
by N in M`, or some such. Most problematically, in the lambda
calculus, an abstract such as `(\x (x x))` is perfectly well-formed
and coherent, but it is not possible to write a `let` expression that
does not have an argument (here, `N`) explicitly present.
Nevertheless, the correspondence is close enough that it can spur
intuition.
Booleans and pairs
==================
So if the lambda calculus can represent any computable function, we
need to do some work to show how to represent some of the functions
we've become acquainted with. We'll start with the `if ... then
... else...` construction.
if x then y else z
For a boolean expression `x`, this complex expression evaluates to `y`
if `x` evaluates to `'true`, and to `z` if `x` evaluations to `'false.
So in order to simulate and `if` clause in the lambda calculus, we
need to settle on a way to represent `'true` and `'false`.
We could simply add the constants `'true` and `'false` to the
language, since it makes sense to add constants to the lambda
calculus. However, that would also require complicating the
interpretation of the language; at the very least, we would need more
than just beta reduction as our engine of computation. In the spirit
of computational minimalism, let's see how far we can get with the
pure lambda calculus, without any special constants.
So we'll need to get rid of the parts of the `if` statement that are
just syntactic window-dressing. That is, we'll need to get rid of the
`if`, the `then`, and the `else`:
M N L
Recall that our convention is that values associate left to right, so
this series of terms is evaluated as
((M N) L)
If this expression is supposed to have the meaning of `if M then N
else L`, then we need to find a value for `'true` such that when it is
substituted in place of `M`, the expression evaluates to `N`. The
function we're looking for takes two arguments: first `N`, then `L`,
throws away `L`, and returns `N`.
We've already seen such a function. We called it **K**: `(\x\y.x)`.
Let's test:
((K M) L) == (((\x\y.x) M) L) ~~> ((\y.M) L) ~~> M
Sucess! In the same spirit, `'false` will be **KI**:
(((K I) M) L) == ((((\x\y.x)(\x.x)) M) L)
~~> (((\y.(\x.x)) M) L)
~~> ((\x.x) L)
~~> L
So have seen our first major representation in the lambda calculus:
"true" is represented by **K**, and "false" is represented by **KI**.
We'll be building up a lot of representations in the weeks to come,
and they will all maintain the discipline that if a expression is
meant to be interpreted as a truth value (i.e., as a Boolean), it will
evaluate to (an expression that is alpha-beta-eta equivalent to) **K**
or **KI**.
Our definition of these is reviewed in [[Assignment 2|topics/assignment2]].
It's possible to do the assignment without using a Scheme interpreter, however
you should take this opportunity to [get Scheme installed on your
computer](/how_to_get_the_programming_languages_running_on_your_computer), and
[get started learning Scheme](/learning_scheme). It will help you test out
proposed answers to the assignment.
There's also a (slow, bare-bones, but perfectly adequate) version of Scheme available for online use at