Chapter 5 the untyped pure lambda calculus
Review the definition and properties of the untyped or pure lambda calculus.
-> The underlying "computational substrate" for most of the type systems.
Context & history
Mid 1960, Peter Landin observed that a complex programming language can be understood by:
The formulation of the language as a tiny core calculus capturing the language's essential mechanisms.
Together with a collection of convenient derived forms whose behavior is understood by translating them into the core.
(Landin, 1964, 1965, 1966; Tennent, 1981)
The core language used by Landin was the lambda-calculus, a formal system invented in the 1920s by (Alonzo Church, 1936, 1941), in which all computation is reduced to the basic operations of function definition and application.
Following Landin's insight, as well as the pioneering work on Lisp (John McCarthy, 1959, 1981), the lambda-calculus has seen widespread use in many areas.
- The specification of programming language features.
- Language design.
- Language implementation.
- The study of type systems.
The lambda calculus's importance arises from the fact that it can be viewed simultaneously as:
- A simple programming language in which computations can be described.
- A mathematical object about which rigorous statements can be proved.
The λ-calculus is just one among many core calculi used to study and define programming languages.
The π-calculus (Milner, Parrow, Walker, 1991-1992) serves as a core language for modeling message-passing concurrency.
Abadi and Cardelli's object calculus (1996) captures the essential features of object-oriented languages.
-> Most of the ideas and techniques for the λ-calculus transfer quite directly to the other calculi.
The λ-calculus can be enriched in several ways.
We can add concrete syntax for features like numbers, tuples, records, etc., whose behavior can already be encoded in the core language, but which are more convenient to work with in "built-in" form.
We can also add more complex features such as:
Mutable reference cells.
Nonlocal exception handling.
These can, in principle, be simulated in the pure λ-calculus, but only via heavy and indirect encodings.
Such extensions gradually lead to full-fledged programming languages, for example:
- ML (Gordon, Milner, Wadsworth, 1979; Milner, Tofte, Harper, 1990; Milner, Tofte, Harper, MacQueen, 1997; etc.)
- Haskell (Hudak et al., 1992)
- Scheme (Sussman and Steele, 1975; Kelsey, Clinger, Rees, 1998)
Extending the core language almost always goes hand-in-hand with extending the type system: richer language features typically require richer typing disciplines to describe and control their behavior.
Basics
Procedural (or functional) abstraction is a key feature of essentially all programming languages.
-> a shorthand for "the function that, for each , yields ".
Example:
-> means "the function applied to ".
Lambda-calculus (or λ-calculus) consists of:
Function definition.
Function application.
…in the purest possible form.
In the lambda-calculus, everything is a function:
- The arguments accepted by functions are themselves functions.
- The result returned by a function is another function.
Lambda-calculus comprises just 3 sorts of terms:
- A variable by itself is a term.
- The abstraction of a variable from a term , written , is a term.
- The application of a term to another term , written , is a term.
The syntax of lambda calculus
Abstract and concrete syntax
It's useful to distinguish two levels of structure:
- Concrete syntax (surface syntax): the actual strings of characters that programmers read and write.
- Abstract syntax: a simpler, internal representation of programs as labeled trees, usually called abstract syntax trees (ASTs).
Abstract syntax trees (ASTs) make the structure of programs explicit:
They represent programs as trees instead of raw text.
This tree structure is ideal for:
- Formal language definitions and proofs about languages.
- The internal workings of compilers and interpreters, which need to manipulate program structure precisely.
Concrete syntax -> Abstract syntax usually happens in two stages:
Lexical analysis (lexing)
A lexer converts the raw character stream into a sequence of tokens: identifiers, keywords, constants, punctuation, etc.
It also:
- Removes comments.
- Handles whitespace and capitalization conventions.
- Deals with formats for numeric and string literals.
Parsing
A parser takes the sequence of tokens and builds an abstract syntax tree.
During parsing, rules like operator precedence and associativity reduce the need for excessive parentheses in the source code.
For example:
- The operator binds more tightly than
+. - So the expression
1 + 2 * 3is parsed as1 + (2 * 3)rather than(1 + 2) * 3.
- The operator binds more tightly than
The main focus is on abstract syntax, not concrete syntax:
- Grammars (like the grammar for λ-terms) should be read as descriptions of legal tree structures, not just valid strings of characters.
- When we write terms in text-examples, definitions, theorems, proofs-we use a linear concrete notation, but we always implicitly mean the corresponding abstract syntax tree.
To avoid writing too many parentheses when λ-terms are written in linear form, we adopt two standard conventions:
Application is left-associative
- The expression
s t uis read as(s t) u, nots (t u).
- The expression
Abstraction bodies extend as far to the right as possible
The expression
λx. λy. x y xis read as:λx. (λy. ((x y) x))
That is, the body of each λ-abstraction includes everything to its right unless parentheses say otherwise.
Variables and metavariables
Metavariables vs object-language variables
- , , (with subscripts) are metavariables for arbitrary terms.
- , , are often metavariables for arbitrary variables (i.e., they range over variable names).
Name overloading & disambiguation
- The same symbols , , etc. are also used as actual variables in the object language (inside terms).
- The intended role-metavariable vs. object-language variable-is always clear from context.
Example: In "The term has the form , where and ":
- and are metavariables.
- and are object-language variables.
Scope
In , is a binder and its scope is the body .
An occurrence of is bound if it appears inside the body of some enclosing abstraction .
An occurrence of x is free if it is not bound by any surrounding abstraction on x.
Examples:
- -> both and are free.
- -> is bound, is free.
- -> is bound.
- -> all occurrences of , , and are bound.
- -> the first (inside ) is bound, the second (the argument) is free.
A term with no free variables is called closed.
Closed terms are also called combinators.
Example: the identity combinator.
- It simply returns its argument unchanged.
Operational semantics
In its pure form, the lambda-calculus has:
- No built-in constants or primitive operators.
- No numbers, arithmetic, conditionals, records, loops, sequencing, or I/O.
- The only way to compute is by applying functions to arguments (and both are just terms).
A single computation step:
Rewrite a function application whose left side is an abstraction.
Substitute the argument for the bound variable in the body.
where [x \mapsto t_2] t_1_2 means "the term obtained by replacing all free occurrences of in t_1_2 with ."
Examples:
Follow Church:
- A term of the form is called a redex (reducible expression).
- The act of rewriting a redex using the rule above is called beta-reduction (\beta-reduction).
Evaluation strategy: Which redex (or redexes) in a term may be reduced at the next evaluation step.
- There can be multiple evaluation strategies for the lambda calculus.
- Different strategies (e.g., normal order, call by value) choose different redexes, but all are based on the same beta-reduction rule above.
Evaluation strategies
Context:
.
Consider this term: .
There are 3 redexes: The outer , the inner , and the inside.
There can be many reductions for the full beta-reduction strategy.
Full beta-reduction
Any redex can be reduced anywhere in the term at any step.
One possible full-reduction sequence (reducing innermost first):
Under the later strategies, the one-step evaluation relation is a partial function: each term has at most one next step.
Normal order
Always reduce the leftmost, outermost redex first.
The normal-order reduction:
Call by name
Like normal order, but no reduction inside abstractions.
Same first steps as normal order, but stop once the result is a \lambda-abstraction:
Here, is treated as a normal form (no further reduction inside the body).
Variants of call by name appear in languages like Algol 60 and in optimized form (call by need), Haskell.
Call by need
An optimized non-strict strategy (used in Haskell).
Similar to call by name, but shares the result of evaluating an argument:
- First use of the argument evaluates it.
- All other uses reuse that value (no re-evaluation).
Conceptually works on graphs (shared structure), not just trees.
Call by value
Only outermost redexes are reduced, and only when the argument is already a value.
The example reduces as:
This is a strict strategy: function arguments are always evaluated, whether or not they are actually used.
In contrast, non-strict (or lazy) strategies like call by name/need only evaluate arguments that are actually used.
The choice of evaluation strategy has little effect on the core issues of type system design. Most typing concepts and techniques apply similarly across strategies.
| Strategy | Which redex is reduced? | Reduce inside \lambda-bodies? | When are arguments evaluated? | Strict? | Pros | Cons |
|---|---|---|---|---|---|---|
| Full \beta-reduction | Any redex, anywhere in the term | Yes | No fixed policy; any redex may be chosen | Not really an execution strategy | Very flexible; useful for theoretical reasoning about equivalence and normalization. | Non-deterministic; not a realistic implementation strategy; no notion of "order of evaluation." |
| Normal order | Leftmost, outermost redex | Yes | Arguments reduced only when needed (when their redex is outermost) | Non-strict | Normalizing: if a normal form exists, this strategy will find it; good for reasoning. | Can be inefficient (may re-evaluate the same argument many times); less practical in naive form. |
| Call by name | Leftmost, outermost redex, but not inside \lambda-abstractions | No | Arguments are substituted but not evaluated inside \lambda-bodies | Non-strict | Captures lazy behavior at a simple semantic level; good as a theoretical model. | Still re-evaluates arguments when used multiple times; not efficient enough by itself. |
| Call by need | Like call by name, but with sharing of evaluated arguments | No | Argument evaluated at most once, then its value is shared | Non-strict (lazy) | Avoids repeated work via sharing; basis for lazy functional languages (e.g. Haskell). | Requires graph-based implementation with sharing; runtime model more complex than CBV. |
| Call by value | Outermost redex whose argument is already a value | Typically no | Arguments evaluated before function body runs | Strict | Simple, efficient, and matches most real-world languages (ML, OCaml, Java, etc.). | Cannot evaluate terms that rely on non-termination avoidance (e.g. some lazy constructions); less flexible than lazy strategies. |
Programming in the lambda calculus
The lambda-calculus has an extremely small core (just variables, abstraction, application) but is surprisingly powerful.
Many language features (multi-argument functions, booleans, pairs, numbers, lists, etc.) can be encoded inside the pure calculus.
Motivations:
- These encodings are "warm-up exercises" to understand the system.
- Not endorsements of \lambda-calculus as a practical programming language.
Multiple arguments (currying)
The pure lambda-calculus has no built-in multi-argument functions.
Instead of writing something like , we use higher-order functions and currying:
Desired informal form:
Curried encoding:
- First apply to : .
- Then apply to : .
This transformation of multi-argument functions to chains of single-argument functions is called currying (after Haskell Curry).
Church booleans
Encode booleans as functions:
A generic "if" (conditional) combinator:
Intuition: the boolean itself acts as the conditional:
- chooses the first argument.
- chooses the second argument.
Boolean operators
- If , result is .
- If , result is .
- So is iff both and are .
Exercise 5.2.1 Define logical and functions.
Solution
Pairs
Encode a pair of values using booleans:
Intuition:
is a function expecting a boolean and returning .
If , we get if , we get .
So:
Church numerals
Encode natural numbers as "iterate a function times":
In general, represents "apply to , times".
Note: and are actually the same term, just used with different intended meanings. This is interesting, as is falsy in most programming languages.
Looks like fixed point approximation?
Successor ():
Addition ():
- First use to apply n times.
- Then apply m more times via .
Multiplication () using plus:
- is a function "add ".
- means "start at , add , times" \to .
Exercise 5.2.2 Find another way to define the successor function on Church numerals.
Solution
Exercise 5.2.3 Is it possible to define multiplication on Church numerals without using ?
Solution
Exercise 5.2.4 Define a term for raising one number to the power of another.
Solution
Zero test:
(no steps applied). - for (at least one overwrite).
Predecessor:
Idea: track pairs . - Helper definitions: - -
Definition: - Starting from , each application of transforms
- After steps, we get or when . - Taking gives the predecessor.
Exercise 5.2.5 Use to define a subtraction function.
Solution
Exercise 5.2.6 Approximately how many steps of evaluation (as a function of ) are required to calculate ?
Solution
TODO
Exercise 5.2.7 Write a function that tests two numbers for equality and returns a Church boolean.
Solution
Other datatypes
Many other structures (lists, trees, variants, arrays) can be encoded similarly.
Exercise 5.2.8 A list can be represented in the lambda-calculus by its function. (OCaml's name for this function is ; it is also sometimes called .) For example, the list becomes a function that takes two arguments and and returns$c\ x\ (c\ y\ (c\ z\ n)))$. What would the representation of be? Write a function that takes an element and a list (that is, a function) and returns a similar representation of the list formed by prepending to . Write and functions, each taking a list parameter. Finally, write a function for this representation of lists (this is quite a bit harder and requires a trick analogous to the one used to define for numbers).
Solution
Observations
The definitions for the Church numerals and lists encoding get me thinking about the technique:
Think of a base value in the domain: for numbers and for lists.
Think of a base operation in the domain that can create any values from the base value: addition for numbers and cons-ing for lists.
Then:
For numerals:
- The 0-th value: .
- The -th value: .
For lists:
- The 0-th value: .
- The 1-st value: .
- The -th value: .
Enriching the calculus
In the pure lambda-calculus we can already encode booleans, numbers, and their operations, so in principle we can write all our programs there.
In practice, it is often more convenient to work in an enriched language that has primitive booleans and numbers as well.
Pure lambda-calculus is denoted by .
No built-in booleans or numbers; everything is encoded.
Pure lambda-calculus enriched with booleans and naturals is denoted by .
Has primitive , , numeric literals, , , , etc. (as in the earlier arithmetic language in Chapter 3. Untyped arithmetic expressions).
In , there are effectively two versions of booleans and numbers:
Real (primitive) values
Encoded (Church) values
Conversions:
Church boolean \to primitive boolean: .
Primitive boolean \to Church boolean: .
Why primitive values help: evaluation order & call by value
We use call by value (CBV):
- Do not reduce under lambdas.
- Only reduce a redex when its argument is already a value.
Example: successor of a Church numeral
- We expect:
- But in CBV we actually get a \lambda-term that is extensionally equal to , but not syntactically the same; some computation is "stuck under a \lambda" and cannot be reduced further under CBV.
- So is behaviorally equivalent to (they act the same when given and ), but not literally the same normal form.
Example: multiplication
does not reduce to under CBV; it reduces to a large lambda-term with a lot of latent computation.
We can still check behavior by:
Comparing at the Church level:
Or more conveniently, converting to a primitive number:
Applying "finishes" the computation, because it supplies the missing arguments ( and ) and forces all remaining \beta-reductions.
Therefore:
- Encodings show that pure is expressive enough.
- Primitives + conversions in make examples and reasoning under call-by-value much easier to see and check.
Recursion
Some terms never reach a normal form; they are said to diverge.
The classic divergent/big omega combinator:
- This term has exactly one redex; reducing it just gives back the same term again, so evaluation loops forever.
- Any term with no normal form (like ) is said to diverge.
The omega combinator has a useful generalization called the fixed-point combinator.
- The call-by-name version is called the Y-combinator.
- The call-by-value version is called the Z-combinator.
The fixed-point combinator is used to specify recursive definitions in the untyped \lambda-calculus:
gives you a term such that, operationally,
so receives a copy of its own result as an argument.
How to derive the call-by-value fixed-point combinator: (Friedman and Felleisen, 1996, Chapter 9).
There is also a simpler call-by-name fixed-point combinator:
but:
diverges under call-by-value.
This is because CBV tries to evaluate too eagerly.
-> It is not usable as-is in the CBV setting.
Using to define recursive functions
We want recursive definitions of the form:
Informal recursive style:
To encode this in \lambda-calculus:
First define a non-recursive generator:
Then define the recursive function as:
Operational idea:
expands to something like .
Everywhere appears in the body of , it is effectively unrolled with another copy of the recursive function.
-> Each recursive call "unrolls" one more copy of the body.
Example: with Church numerals
The machinery ensures that each time is applied.
-> effectively behaves like factorial itself.
-> The definition is unrolled one step at a time.
Conceptually, is a self-replicator:
Applying feeds and back into .
-> Producing another expanded copy of the recursive body, with new $\text{fct}$s ready to continue the process.
Exercise 5.2.9 Why did we use a primitive in the definition of , instead of the Church-boolean function on Church booleans? Show how to define the function in terms of rather than .
Solution
- This is because doesn't need to evaluate both of its branches before evaluating itself, while needs to evaluate both of its branches first.
- In other words, is lazy and is eager.
- If we use in the same way as in the above example, it would yield divergent terms on every application.
- How to define in terms of : Simulate call-by-name using thunks.
Exercise 5.2.10 Define a function that converts a primitive natural number into the corresponding Church numeral.
Solution
Exercise 5.2.11 Use and the encoding of lists from Exercise 5.2.8 to write a function that sums lists of Church numerals.
Solution
Representation
What does it mean that Church numerals "represent" ordinary numbers?
Ordinary naturals (as in ):
A constant: .
Operations:
- : numbers numbers.
- : numbers numbers.
- : numbers booleans.
Their behavior is fixed by evaluation rules (e.g. , ).
Church encoding idea: represent all of these as lambda-terms.
Zero:
- Other behaviorally equivalent (non-canonical) terms (e.g. ) also count as representations of 0.
Successor/predecessor:
- represents : if represents , then evaluates to a representation of .
- represents : if represents , then evaluates to a representation of .
Zero test: represents :
- If represents 0, evaluates to .
- If represents any , evaluates to .
Representation correctness (observational view):
Take any program that:
- Uses primitive numbers and operations (, , , , \ldots)
- Produces a boolean result.
Replace all numbers and arithmetic operations with their Church encodings (, , , , \ldots).
After evaluation, the final boolean result is the same.
Therefore, no observable difference: Church numerals and primitive naturals behave the same from the program's point of view.
Formalities
Syntax
The usual \lambda-calculus grammar (e.g. ) is shorthand for an inductively defined set of abstract syntax trees.
Terms:
Fix a countable set of variable names .
The set of terms is the smallest set such that:
- .
- .
- .
Free variables ()
= set of variables that occur free in term .
Rules:
- .
- .
- .
Exercise 5.3.3 Give a careful proof that for every term .
Solution
TODO
Substitution
Throughout, two definitions of substitution are used:
- The compact and intuitive definition shown before: , optimized for examples and in mathematical definitions and proofs.
- Another developed in Chapter 6, is notationally heavier, depending on an alternative "de Bruijn presentation" of terms in which named variables are replaced by numeric indices, but is more convenient for the concrete ML implementations.
Goal: define substitution in the \lambda-calculus correctly (capture-avoiding).
Naive substitution (wrong #1)
Defined structurally on term :
- .
Problem: does not distinguish free vs bound occurrences of .
Example:
-> This conflicts with the basic intuition about functional abstractions that the names of bound variables do not matter: The identity function is exactly the same whether we write it or or . If these do not behave exactly the same under substitution, then they will not behave the same under reduction either, which seems wrong.
Mistake: No distinction between free occurrences of a variable (which should get replaced during substitution) and bound ones, which should not.
Improved substitution (wrong #2) - stop at binder with the same name
Modify abstraction case to not substitute under a binder with the same name:
Fixes earlier issue, but introduces variable capture:
Example:
Variable capture & capture-avoiding substitution
Variable capture: a free variable in becomes bound after substitution into .
To avoid this, in the abstraction case we must ensure the bound variable :
- is not , and
- does not occur free in .
Capture-avoiding substitution:
The above definition is partial: if but , no clause applies.
-> Work with terms "up to renaming bound variables".
Alpha-conversion
\alpha-conversion: Consistent renaming of a bound variable - .
Convention: Terms that differ only by renaming bound variables are interchangeable - We work "up to \alpha-conversion".
If substitution would be undefined because , we rename the bound variable first.
Example:
- First rename: \to
- Then:
Final capture-avoiding substitution definition
Using the \alpha-conversion convention, we assume the binder is always chosen fresh (\neq and not free in ), so we can drop the special case:
This is the standard capture-avoiding substitution used in \lambda-calculus proofs and definitions.
Operational semantics (lambda calculus)
The untyped lambda calculus \lambda
Values are exactly lambda-abstractions:
- Evaluation stops when it reaches a ; arbitrary -terms can be values.
Small-step rules (call-by-value application)
E-AppAbs- computation ruleE-App1****(reduce the function part first) - congruence ruleE-App2(then reduce the argument) - congruence rule
How metavariables enforce evaluation order
in
E-AppAbs: must be a value, so \beta-reduction only fires when the argument is fully evaluated.in
E-App1: any term; we reduce the function position first while it can step.in
E-App2: the left side must already be a value, so we only start reducing the argument after the function is done.Combined, these rules enforce call-by-value, left-to-right:
- Reduce to a value (
E-App1). - Reduce to a value (
E-App2). - Apply \beta (
E-AppAbs).
- Reduce to a value (
Special property of the pure \lambda-calculus
- Since only \lambda-abstractions are values, once has been reduced to a value (by
E-App1), it must be a . - This breaks once we enrich the language (e.g. add primitive booleans, numbers), where values include more forms than just \lambda-abstractions.
- Since only \lambda-abstractions are values, once has been reduced to a value (by
Exercise 5.3.6 Adapt these rules to describe the other three strategies for evaluation-full beta-reduction, normal-order, and lazy evaluation.
Solution
Full beta-reduction
Normal-order
Call-by-name
Exercise 5.3.7 Exercise 3.5.16 gave an alternative presentation of the operational semantics of booleans and arithmetic expressions in which stuck terms are defined to evaluate to a special constant wrong. Extend this semantics to .
Solution
TODO
Exercise 5.3.8 Exercise 4.2.2 introduced a "big-step" style of evaluation for arithmetic expressions, where the basic evaluation relation is "term evaluates to final result ." Show how to formulate the evaluation rules for lambda-terms in the big-step style.