Functions as rules
In school a function is often a formula: . In set theory it becomes a set of ordered pairs. In the early 1930s the American logician Alonzo Church took a third view: a function is a rule that can be applied, and the only things in the world are rules. He wrote for "the function that sends to ", and studied what can be done with abstraction () and application () alone.
The answer is: everything that can be computed. Truth values, pairs, natural numbers and arithmetic can be encoded as pure functions; recursion comes from a clever self-application; and the resulting calculus computes exactly the same functions as Turing machines. Python's lambda, JavaScript's arrow functions and every functional programming language descend from it.
It also contains a warning. Type this into Python:
(lambda x: x(x))(lambda x: x(x))
It never finishes (Python gives up with a RecursionError). In the λ-calculus this term is called , and it reduces to itself forever. So the λ-calculus raises two questions that this lesson answers. If a computation can be carried out in many orders, can different orders give different answers? (No — the Church–Rosser theorem.) And which terms are guaranteed to finish? (Among others, all typed ones.) The lesson assumes induction from proof techniques and some experience of programming.
Terms as trees, reduction as plugging in
A λ-term is a tree built from variables, applications and abstractions. In the variable is bound by the — it is a placeholder, and means the same thing — while is free. Computing means β-reduction: wherever a function meets an argument, plug the argument in,
The only subtlety is that plugging in must not change the meaning of free variables. Take , the function that returns a constant function. Applied to a free variable , naive textual substitution would give
— the free has been captured by the inner , and a constant function has turned into the identity. The correct answer renames the bound variable first: . Capture-avoiding substitution is easy to state and notoriously easy to get wrong; the Lean formalisation sidesteps it entirely by using de Bruijn indices.
Definitions
Definition— λ-terms
Given an infinite set of variables , the λ-terms are generated by
Conventions: application associates to the left (); the body of a extends as far right as possible; abbreviates .
Definition— free variables and α-equivalence
, , . A term is closed if it has no free variables. Terms that differ only by renaming bound variables (such as and ) are α-equivalent, and we identify them.
Definition— capture-avoiding substitution
replaces the free occurrences of in by :
- , and for ;
- ;
- ;
- if and ; otherwise first rename to a variable not free in or .
Definition— β-reduction and normal forms
A redex is a term ; its contractum is . One-step β-reduction contracts one redex anywhere inside . Write for zero or more steps, and for the equivalence relation generated by (β-equality). A term with no redex is in normal form; has a normal form if with normal. is weakly normalising if it has a normal form, and strongly normalising if every reduction sequence from is finite.
Definition— de Bruijn indices
Replace each variable occurrence by the number of 's between it and its binder: becomes and becomes . α-equivalent terms become identical, and substitution becomes arithmetic on indices, with no renaming. This is how the Lean formalisation and the Python experiments represent terms.
Examples: combinators, data and recursion
Example— Small combinators
, , . Then , , and
so behaves like . With and :
Example— Order matters for termination
has two redexes. Contracting the leftmost-outermost one first: , a normal form in two steps. Contracting first does nothing: , forever. A term can have a normal form and an infinite reduction. Normal order — always contract the leftmost-outermost redex — finds the normal form whenever there is one (the standardisation theorem, below); applicative order — evaluate arguments first, as most programming languages do — can loop.
Example— Booleans and pairs
A truth value is a choice: picks the first of two arguments, the second. Then "if then else " is just , and
For instance . A pair is a function waiting for a selector: , , .
Example— Church numerals
The number is "do something times": , so and . Arithmetic is composition of iterations:
In normal order, takes six steps:
Testing for zero is . The predecessor is harder — Church at first doubted that it could be defined; Stephen Kleene found it by iterating on pairs, , then taking the first component:
Example— Recursion by self-application
Curry's paradoxical combinator and Turing's with produce fixed points (Theorem 1). To define the factorial, solve the recursive equation for
by . Then — after 1477 normal-order steps in the experiment below. The same trick with gives with , a term with no normal form: recursion and non-termination are two sides of one coin.
The main theorems
Theorem— Theorem 1 (fixed points)
For every term there is a term with . In fact , and and have a common reduct.
Proof
With : . For , let . Then , and also . So (or ) works.
∎
Now the central theorem. The naive approach — show that two one-step reducts can be joined in one step each, then tile — fails, because contracting one redex can duplicate another: from one step gives and another gives , and joining them at takes two steps on the left. The cure (Tait and Martin-Löf) is to reduce in parallel.
Definition— parallel reduction
is defined inductively: ; if then ; if and then and . Intuitively: contract any set of redexes already present, all at once.
Theorem— Lemma A
(i) . (ii) If then . (iii) If then . Hence is the reflexive–transitive closure of .
Proof
(i) By induction on . (ii) By induction on the position of the contracted redex, using (i) for the parts that do not change. (iii) By induction on the derivation of : the congruence cases chain reductions of the parts, and the last case reduces and inside and then contracts. Finally, (ii) and (iii) give , and taking reflexive–transitive closures of all three gives .
∎
We use the substitution lemma: if and , then . (Induction on : for both sides are ; for both are , using ; the other cases are immediate, choosing bound variables fresh.)
Theorem— Lemma B (parallel reduction respects substitution)
If and , then .
Proof
Induction on the derivation of , with bound variables chosen different from and not free in . Variable case: if the claim is ; otherwise both sides are the same variable. The abstraction and application cases follow from the induction hypothesis. In the remaining case and with and . Then
using the induction hypothesis for and , then the substitution lemma (valid because ).
∎
Definition— complete development
contracts every redex of : ; ; if is not an abstraction; .
Theorem— Lemma C (triangle property; Takahashi)
If then . Consequently has the diamond property: if and , then and .
Proof
Induction on the derivation of . Variables and abstractions are immediate. If and with , , then by induction and , and Lemma B gives . If and with , : when is not an abstraction, and the induction hypotheses give . When , the only rule producing makes with , and the induction hypothesis gives . So by the last rule of .
∎
Theorem— Lemma D (diamond implies confluence)
If a relation has the diamond property, then so does its reflexive–transitive closure : if and , there is with and .
Proof
First the strip lemma: if and , then there is with and . By induction on the length of : for length take ; if and (by induction) , , the diamond property applied to and gives with and ; then . Now induct on the length of in the same way, using the strip lemma for each step. Geometrically: the two reductions span a grid, and each square is filled by one diamond.
∎
Theorem— Theorem 2 (Church–Rosser)
β-reduction is confluent: if and , then there is a term with and .
Proof
By Lemma A, equals . By Lemma C, has the diamond property, so by Lemma D, is confluent.
∎
Theorem— Corollary 3 (uniqueness of normal forms; consistency)
- If , then and have a common reduct.
- A term has at most one normal form.
- Distinct normal forms are not β-equal. In particular and , so β-equality does not identify all terms.
Proof
(1) By induction on a chain in which each consecutive pair is related by or : if and have a common reduct , then either , so is also a reduct of , or , and Church–Rosser applied to and gives a common reduct of and , hence of and . (2) If are normal forms of , Church–Rosser gives with ; a normal form reduces only to itself, so . (3) follows from (1) in the same way.
∎
So the value of a computation, if it has one, does not depend on the order of evaluation. Two further classical theorems complete the picture; we state them with references.
Theorem— Theorem 4 (standardisation, stated)
If has a normal form, then normal-order reduction (always contracting the leftmost-outermost redex) reaches it. (Curry and Feys 1958; see Barendregt 1984 or Hindley and Seldin 2008.)
Theorem— Theorem 5 (λ-definability and undecidability, stated)
A partial function is λ-definable on Church numerals if and only if it is partial recursive, if and only if it is computable by a Turing machine (Kleene 1936, Turing 1937). Consequently there is no algorithm that decides whether a λ-term has a normal form, or whether two λ-terms are β-equal (Church 1936).
The idea of the first equivalence: primitive recursion is iteration on pairs, exactly as in , and unbounded search ("the least with ") is a recursive definition, solved with . Conversely, β-reduction is a mechanical process a Turing machine can simulate. See computability.
Types tame the calculus
The simply typed λ-calculus attaches a type to every variable. Types are built from base types by ; the rules are: a variable has type ; if and then ; and if assuming , then . So and , but has no type: would need and at once, i.e. , impossible since is strictly bigger than . Types are preserved by reduction (subject reduction, a consequence of a substitution lemma for typings).
The degree of a redex is the size (number of arrows) of the type of the function .
Theorem— Theorem 6 (weak normalisation; Turing)
Every simply typed λ-term has a normal form.
Proof
For a term let be the maximum degree of its redexes and the number of redexes of degree ; we induct on in lexicographic order. If has no redex we are done. Otherwise choose a redex of degree that is rightmost among those of degree (its comes last in the written term). Every redex inside starts to the right of 's , so contains no redex of degree . Contract to and look at the redexes of the result:
- redexes outside , and redexes inside , survive exactly once each, with the same degree (substitution does not copy );
- redexes inside may be copied many times, but all have degree ;
- new redexes can be created only in two ways: where contains and is an abstraction, giving of degree ; or where itself was applied, , and is an abstraction, giving a redex of degree , where is the type of .
So the redex of degree is gone and no redex of degree was created: either drops by one or, if was , the maximal degree drops. By induction the process reaches a normal form.
∎
Theorem— Theorem 7 (strong normalisation and expressive power, stated)
Every simply typed λ-term is strongly normalising: every reduction sequence is finite (Tait 1967, by "reducibility candidates"; see Girard, Lafont and Taylor 1989). The price is expressive power: on Church numerals of type , the simply typed λ-calculus defines exactly the extended polynomials — functions built from constants, addition, multiplication and a test for zero (Schwichtenberg 1976). Not even subtraction or exponentiation in general is definable at that type.
So typed programs always terminate, but some computable functions cannot be typed this way — an instance of a general trade-off: a total language cannot define all computable total functions. Under the Curry–Howard correspondence, types are propositions and typed terms are proofs; normalisation of terms is normalisation of proofs. That is the subject of type theory and propositions as types.
The λ-calculus in Lean
The Lean module uses de Bruijn indices, so α-equivalent terms are equal and substitution never captures. Substitutions are simultaneous: subst σ t replaces each free variable n by σ n, and going under a binder uses up σ, which shifts the substituted terms. The β-rule contracts (λ t) s to inst t s.
/-- Untyped λ-terms with **de Bruijn indices**: `var n` refers to the variable bound by the
`n`-th enclosing `lam`, counting outwards from `0`. So `λx. λy. x` is `lam (lam (var 1))`, and
α-equivalent terms such as `λx. x` and `λy. y` are the same term `lam (var 0)`. -/
inductive Term where
| var : Nat → Term
| app : Term → Term → Term
| lam : Term → Term
deriving DecidableEq, Repr/-- Lift a substitution `σ` (a term for each free variable) under one binder: the new bound
variable stays put, and the substituted terms are shifted so that their free variables skip the
new binder. This shift is exactly what makes substitution **capture-avoiding**. -/
def up (σ : Nat → Term) : Nat → Term
| 0 => var 0
| n + 1 => rename Nat.succ (σ n)/-- Simultaneous capture-avoiding substitution: replace each free variable `n` by `σ n`. -/
def subst (σ : Nat → Term) : Term → Term
| var n => σ n
| app s t => app (subst σ s) (subst σ t)
| lam t => lam (subst (up σ) t)/-- `inst t s` is `t[s/x]` for the variable `x` bound by an enclosing `λ`: index `0` becomes
`s`, and the other free indices drop by one because that `λ` has disappeared. -/
def inst (t s : Term) : Term :=
subst (scons s var) tThe algebra of substitution is proved from scratch. Two of the fusion laws, including the substitution lemma used in Lemma B:
/-- **Substitution is associative**: substituting `σ` and then `τ` is substituting the
composite `n ↦ subst τ (σ n)`. -/
theorem subst_subst (t : Term) : ∀ (σ τ : Nat → Term),
subst τ (subst σ t) = subst (subst τ ∘ σ) t := by
induction t with
| var n => intro σ τ; rfl
| app s t ihs iht => intro σ τ; simp [subst, ihs, iht]
| lam t ih =>
intro σ τ
simp only [subst, ih]
congr 2
funext n
cases n with
| zero => rfl
| succ k =>
show subst (up τ) (rename Nat.succ (σ k)) = rename Nat.succ (subst τ (σ k))
rw [subst_rename, rename_subst]
rfl/-- **The substitution lemma**: `(t[s/x])σ = (tσ')[sσ/x]`, where `σ'` is `σ` lifted under the
binder of `x`. -/
theorem subst_inst (σ : Nat → Term) (t s : Term) :
subst σ (inst t s) = inst (subst (up σ) t) (subst σ s) := by
simp only [inst, subst_subst]
congr 1
funext n
cases n with
| zero => rfl
| succ k =>
show σ k = subst (scons (subst σ s) var) (rename Nat.succ (σ k))
rw [subst_rename, subst_var]
intro m
rflβ-reduction, normal forms and Ω. reduces only to itself, so it has no normal form:
/-- One step of **β-reduction**, anywhere in a term: `(λx. t) s → t[s/x]`, also inside
applications and under λ. -/
inductive Step : Term → Term → Prop where
| beta (t s : Term) : Step (app (lam t) s) (inst t s)
| appL {s s' : Term} (t : Term) : Step s s' → Step (app s t) (app s' t)
| appR (s : Term) {t t' : Term} : Step t t' → Step (app s t) (app s t')
| lam {t t' : Term} : Step t t' → Step (lam t) (lam t')/-- A term is in **normal form** if no β-step applies to it. -/
def Normal (t : Term) : Prop :=
∀ u, ¬ Step t u/-- `Ω = ω ω = (λx. x x)(λx. x x)`. -/
def bigOmega : Term := app omega omega/-- **Ω reduces to itself and to nothing else.** The only redex is the whole term, and
contracting it substitutes `ω` for `x` in `x x`, giving `ω ω` back. -/
theorem bigOmega_step (u : Term) : Step bigOmega u ↔ u = bigOmega := by
constructor
· intro h
cases h with
| beta => rfl
| appL _ h' => exact absurd h' (omega_normal _)
| appR _ h' => exact absurd h' (omega_normal _)
· rintro rfl
exact Step.beta _ _/-- **Ω has no normal form**: everything it reduces to is Ω, and Ω is not normal. So the untyped
λ-calculus has non-terminating computations. -/
theorem bigOmega_no_normal_form : ¬ ∃ u, ReflTransGen Step bigOmega u ∧ Normal u := by
rintro ⟨u, hu, hn⟩
have only : ∀ w, ReflTransGen Step bigOmega w → w = bigOmega := by
intro w hw
induction hw with
| refl => rfl
| tail _ hstep ih =>
subst ih
exact (bigOmega_step _).1 hstep
have := only u hu
subst this
exact hn bigOmega ((bigOmega_step bigOmega).2 rfl)Fixed points (Theorem 1).
/-- **Turing's fixed-point combinator** `Θ = A A` with `A = λx. λy. y (x x y)`. -/
def theta : Term :=
app (lam (lam (app (var 0) (app (app (var 1) (var 1)) (var 0)))))
(lam (lam (app (var 0) (app (app (var 1) (var 1)) (var 0)))))/-- **`Θ F ↠ F (Θ F)` for every term `F`** — in two β-steps. This is how the untyped calculus
defines recursive functions: a solution of `f = F f`. -/
theorem theta_fixed_point (F : Term) : ReflTransGen Step (app theta F) (app F (app theta F)) := by
refine ReflTransGen.head (Step.appL F (Step.beta _ _)) ?_
exact ReflTransGen.single (Step.beta _ _)/-- **Curry's paradoxical combinator** `Y = λf. (λx. f (x x)) (λx. f (x x))`. -/
def curryY : Term :=
lam (app (lam (app (var 1) (app (var 0) (var 0))))
(lam (app (var 1) (app (var 0) (var 0)))))/-- **`Y F` and `F (Y F)` reduce to a common term**, so they are β-equal. (Unlike `Θ`, `Y F`
does not reduce to `F (Y F)` itself.) With `W = λx. F (x x)`: `Y F → W W → F (W W)` and
`F (Y F) → F (W W)`. -/
theorem curryY_fixed_point (F : Term) :
∃ N, ReflTransGen Step (app curryY F) N ∧ ReflTransGen Step (app F (app curryY F)) N := by
let W : Term := lam (app (rename Nat.succ F) (app (var 0) (var 0)))
have h1 : Step (app curryY F) (app W W) := Step.beta _ _
have h2 : Step (app W W) (app F (app W W)) := by
have e : inst (app (rename Nat.succ F) (app (var 0) (var 0))) W = app F (app W W) := by
simp only [inst, subst]
rw [subst_rename, subst_var]
· rfl
· intro n
rfl
rw [← e]
exact Step.beta _ _
exact ⟨app F (app W W), ReflTransGen.head h1 (ReflTransGen.single h2),
ReflTransGen.single (Step.appR F h1)⟩Church numerals, computed by the kernel. A normal-order evaluator is proved sound (each step is a β-step) and complete for normal forms (when it stops, the term is normal). Then decide makes Lean's kernel run it, and soundness turns the run into a proof of .
/-- The **Church numeral** `n̄ = λf. λx. fⁿ x`: the number `n` as "do something `n` times". -/
def church (n : Nat) : Term := lam (lam (iterate n))/-- `plus = λm. λn. λf. λx. m f (n f x)`: do `f` `n` times, then `m` more times. -/
def plus : Term :=
lam (lam (lam (lam (app (app (var 3) (var 1)) (app (app (var 2) (var 1)) (var 0))))))/-- One step of **normal-order** (leftmost-outermost) reduction, or `none` if the term is
normal: contract the head redex if there is one, otherwise reduce inside, left to right. -/
def stepNO : Term → Option Term
| var _ => none
| lam t => (stepNO t).map lam
| app (lam t) s => some (inst t s)
| app (var n) t => (stepNO t).map (app (var n))
| app (app s₁ s₂) t =>
match stepNO (app s₁ s₂) with
| some s' => some (app s' t)
| none => (stepNO t).map (app (app s₁ s₂))/-- **The evaluator is sound**: every step it takes is a β-step. -/
theorem stepNO_sound (t : Term) : ∀ u, stepNO t = some u → Step t u := by
induction t with
| var n => intro u h; cases h
| lam t ih =>
intro u h
simp only [stepNO, Option.map_eq_some_iff] at h
obtain ⟨t', ht', rfl⟩ := h
exact Step.lam (ih t' ht')
| app s t ihs iht =>
intro u h
cases s with
| var n =>
simp only [stepNO, Option.map_eq_some_iff] at h
obtain ⟨t', ht', rfl⟩ := h
exact Step.appR _ (iht t' ht')
| lam s₀ =>
simp only [stepNO, Option.some.injEq] at h
subst h
exact Step.beta _ _
| app s₁ s₂ =>
simp only [stepNO] at h
split at h
· rename_i s' hs'
simp only [Option.some.injEq] at h
subst h
exact Step.appL _ (ihs s' hs')
· simp only [Option.map_eq_some_iff] at h
obtain ⟨t', ht', rfl⟩ := h
exact Step.appR _ (iht t' ht')/-- **When the evaluator stops, the term is normal**: if normal order finds no redex, there is
no redex at all. -/
theorem stepNO_none (t : Term) : stepNO t = none → Normal t := by
induction t with
| var n => intro _ u h; cases h
| lam t ih =>
intro h u hu
simp only [stepNO, Option.map_eq_none_iff] at h
cases hu with
| lam h' => exact ih h _ h'
| app s t ihs iht =>
intro h u hu
cases s with
| var n =>
simp only [stepNO, Option.map_eq_none_iff] at h
cases hu with
| appL _ h' => cases h'
| appR _ h' => exact iht h _ h'
| lam s₀ => simp [stepNO] at h
| app s₁ s₂ =>
simp only [stepNO] at h
split at h
· cases h
· rename_i hs
simp only [Option.map_eq_none_iff] at h
cases hu with
| appL _ h' => exact ihs hs _ h'
| appR _ h' => exact iht h _ h'/-- **`plus 2̄ 3̄ ↠ 5̄`**. The kernel runs the evaluator (6 normal-order steps) and checks that
the result is the Church numeral `5̄`; soundness turns the run into a β-reduction. -/
theorem two_plus_three : ReflTransGen Step (app (app plus (church 2)) (church 3)) (church 5) := by
have h : normalize 20 (app (app plus (church 2)) (church 3)) = church 5 := by decide
rw [← h]
exact normalize_steps 20 _/-- **`times 2̄ 3̄ ↠ 6̄`**, computed the same way. -/
theorem two_times_three : ReflTransGen Step (app (app times (church 2)) (church 3)) (church 6) := by
have h : normalize 30 (app (app times (church 2)) (church 3)) = church 6 := by decide
rw [← h]
exact normalize_steps 30 _Lemma D, abstractly, proved from scratch and compared with Mathlib's Relation.church_rosser:
/-- A relation has the **diamond property** if any two one-step reducts of a term can be
joined by one more step on each side. -/
def Diamond : Prop :=
∀ a b c, r a b → r a c → ∃ d, r b d ∧ r c d/-- **The strip lemma**: if `r` has the diamond property, a single `r`-step and a many-step
`r`-reduction from the same term can be joined (one step on the far side, many on the other).
Induction along the many-step reduction, tiling one diamond at a time. -/
theorem strip (hd : Diamond r) {a b c : α} (hab : r a b) (hac : ReflTransGen r a c) :
∃ d, ReflTransGen r b d ∧ r c d := by
induction hac with
| refl => exact ⟨b, ReflTransGen.refl, hab⟩
| tail _ hcc' ih =>
obtain ⟨d, hbd, hcd⟩ := ih
obtain ⟨e, hde, hc'e⟩ := hd _ _ _ hcd hcc'
exact ⟨e, hbd.tail hde, hc'e⟩/-- **Diamond implies confluence**: if `r` has the diamond property, then any two many-step
reducts of a term have a common many-step reduct. Induction along one reduction, using the
strip lemma for each of its steps. -/
theorem diamond_confluent (hd : Diamond r) {a b c : α} (hab : ReflTransGen r a b)
(hac : ReflTransGen r a c) : ∃ d, ReflTransGen r b d ∧ ReflTransGen r c d := by
induction hab with
| refl => exact ⟨c, hac, ReflTransGen.refl⟩
| tail _ hbb' ih =>
obtain ⟨d, hbd, hcd⟩ := ih
obtain ⟨e, hb'e, hde⟩ := strip hd hbb' hbd
exact ⟨e, hb'e, hcd.tail hde⟩/-- The same abstract fact from Mathlib, in a slightly stronger form (Mathlib's
`Relation.church_rosser` only needs the joining step on one side to be optional). -/
theorem church_rosser_library (hd : Diamond r) {a b c : α} (hab : ReflTransGen r a b)
(hac : ReflTransGen r a c) : Join (ReflTransGen r) b c :=
Relation.church_rosser (fun a b c hab hac =>
let ⟨d, hbd, hcd⟩ := hd a b c hab hac
⟨d, ReflGen.single hbd, ReflTransGen.single hcd⟩) hab hacThe Church–Rosser theorem (Theorem 2 and Corollary 3), following Lemmas A–C: parallel reduction, its substitution lemma, complete developments and the triangle property.
/-- **Parallel reduction** `t ⇒ t'` (Tait, Martin-Löf): contract any set of redexes already
present in `t`, all at once. Unlike `→`, it has the diamond property. -/
inductive Par : Term → Term → Prop where
| var (n : Nat) : Par (var n) (var n)
| app {s s' t t' : Term} : Par s s' → Par t t' → Par (app s t) (app s' t')
| lam {t t' : Term} : Par t t' → Par (lam t) (lam t')
| beta {t t' s s' : Term} : Par t t' → Par s s' → Par (app (lam t) s) (inst t' s')/-- **Parallel reduction is preserved by substitution**: if `t ⇒ t'` and `σ n ⇒ τ n` for every
variable `n`, then `tσ ⇒ t'τ`. The β case is the substitution lemma `subst_inst`. -/
theorem par_subst {t t' : Term} (h : Par t t') :
∀ σ τ, (∀ n, Par (σ n) (τ n)) → Par (subst σ t) (subst τ t') := by
induction h with
| var n => intro σ τ hστ; exact hστ n
| app _ _ ihs iht => intro σ τ hστ; exact Par.app (ihs σ τ hστ) (iht σ τ hστ)
| lam _ ih =>
intro σ τ hστ
refine Par.lam (ih _ _ ?_)
intro n
cases n with
| zero => exact Par.var 0
| succ k => exact par_rename (hστ k) _
| beta _ _ iht ihs =>
intro σ τ hστ
rw [subst_inst]
refine Par.beta (iht _ _ ?_) (ihs σ τ hστ)
intro n
cases n with
| zero => exact Par.var 0
| succ k => exact par_rename (hστ k) _/-- **The complete development** `t*` (Takahashi): contract *every* redex present in `t`,
innermost information first. -/
def cd : Term → Term
| var n => var n
| lam t => lam (cd t)
| app (lam t) s => inst (cd t) (cd s)
| app (var n) t => app (var n) (cd t)
| app (app s₁ s₂) t => app (cd (app s₁ s₂)) (cd t)/-- **The triangle property**: if `t ⇒ u`, then `u ⇒ t*`. Every parallel reduct of `t` can
catch up with the complete development of `t` in one more parallel step. -/
theorem triangle {t u : Term} (h : Par t u) : Par u (cd t) := by
induction h with
| var n => exact Par.var n
| lam _ ih => exact Par.lam ih
| beta _ _ iht ihs => exact par_inst iht ihs
| @app s s' t t' hs _ ihs iht =>
cases s with
| var n => exact Par.app ihs iht
| app s₁ s₂ => exact Par.app ihs iht
| lam s₀ =>
cases hs with
| lam hs₀ =>
cases ihs with
| lam ih₀ => exact Par.beta ih₀ iht/-- Parallel reduction has the **diamond property**: two parallel reducts of `t` both reduce in
one parallel step to the complete development `t*`. -/
theorem par_diamond : Diamond Par :=
fun t _ _ hu hv => ⟨cd t, triangle hu, triangle hv⟩/-- **The Church–Rosser theorem** (Church and Rosser, 1936): β-reduction is confluent. If
`t ↠ u` and `t ↠ v`, there is a `w` with `u ↠ w` and `v ↠ w`. Proof: confluence of parallel
reduction (diamond ⇒ confluence), transported along `↠ = ⇒*`. -/
theorem church_rosser {t u v : Term} (hu : ReflTransGen Step t u) (hv : ReflTransGen Step t v) :
∃ w, ReflTransGen Step u w ∧ ReflTransGen Step v w := by
obtain ⟨w, huw, hvw⟩ :=
diamond_confluent par_diamond (steps_iff_par_steps.1 hu) (steps_iff_par_steps.1 hv)
exact ⟨w, steps_iff_par_steps.2 huw, steps_iff_par_steps.2 hvw⟩/-- **Normal forms are unique**: a term has at most one normal form. By Church–Rosser two normal
forms `u, v` of `t` reduce to a common `w`; a normal term reduces only to itself, so
`u = w = v`. -/
theorem normal_form_unique {t u v : Term} (hu : ReflTransGen Step t u) (hv : ReflTransGen Step t v)
(nu : Normal u) (nv : Normal v) : u = v := by
have stuck : ∀ {x y : Term}, Normal x → ReflTransGen Step x y → x = y := by
intro x y nx hxy
cases hxy.cases_head with
| inl h => exact h
| inr h =>
obtain ⟨z, hxz, _⟩ := h
exact absurd hxz (nx z)
obtain ⟨w, huw, hvw⟩ := church_rosser hu hv
rw [stuck nu huw, stuck nv hvw]/-- Consequently `5̄` is *the* normal form of `plus 2̄ 3̄`: no reduction strategy can produce a
different normal form. -/
theorem plus_unique_value {v : Term} (hv : ReflTransGen Step (app (app plus (church 2)) (church 3)) v)
(nv : Normal v) : v = church 5 :=
(normal_form_unique two_plus_three hv (church_normal 5) nv).symmWhat is and is not formalised. Everything above — syntax, substitution, β-reduction, , both fixed-point combinators, Church arithmetic, the evaluator and the full Church–Rosser theorem with uniqueness of normal forms — is proved independently in this module; from Mathlib it uses only the reflexive–transitive closure ReflTransGen (and Relation.church_rosser, for the comparison above). Not formalised: standardisation (Theorem 4), λ-definability of the partial recursive functions and undecidability (Theorem 5), and the typed calculus with Theorems 6 and 7.
An interpreter, and Church–Rosser by exhaustion
The first experiment is a small λ-calculus interpreter. It parses named terms into de Bruijn form, reduces them by normal order, and counts β-steps. It also runs under applicative order.
"""A small lambda-calculus interpreter: Church numerals, recursion, and loops.
Terms are written in the usual named syntax (``\\x. x`` or ``λx. x``), parsed
into de Bruijn indices, and reduced with capture-avoiding substitution. Normal
order (leftmost-outermost redex first) computes Church-numeral arithmetic,
booleans, Kleene's predecessor and a factorial defined with the fixed-point
combinator Y, counting beta-steps. Omega = (λx. x x)(λx. x x) runs out of fuel,
and K I Omega shows that the strategy matters: normal order finds the normal
form, applicative order (arguments first) never does. Every result is an exact
computation for these particular terms.
"""
import re
# A term is ("var", index), ("app", function, argument) or ("lam", body).
def parse(text, defs=None):
"""Parse a named λ-term into de Bruijn form.
Syntax: ``\\x. body`` or ``λx. body`` (abstraction extends to the right),
application by juxtaposition (left-associative), parentheses, and names
from ``defs`` (a dict name -> already-parsed closed term) used as macros.
"""
defs = defs or {}
tokens = re.findall(r"[\\λ.()]|[A-Za-z0-9_']+", text)
pos = 0
def peek():
return tokens[pos] if pos < len(tokens) else None
def take(expected=None):
nonlocal pos
tok = peek()
if tok is None or (expected is not None and tok != expected):
raise ValueError(f"expected {expected!r} at token {pos}: {text!r}")
pos += 1
return tok
def term(scope):
if peek() in ("\\", "λ"):
take()
name = take()
take(".")
return ("lam", term([name] + scope))
result = None
while peek() not in (None, ")"):
arg = atom(scope)
result = arg if result is None else ("app", result, arg)
if peek() in ("\\", "λ"):
arg = term(scope)
result = ("app", result, arg)
if result is None:
raise ValueError(f"empty term: {text!r}")
return result
def atom(scope):
tok = take()
if tok == "(":
inner = term(scope)
take(")")
return inner
if tok in scope:
return ("var", scope.index(tok))
if tok in defs:
return defs[tok]
raise ValueError(f"unbound name {tok!r} in {text!r}")
result = term([])
if pos != len(tokens):
raise ValueError(f"unexpected {peek()!r} in {text!r}")
return result
def shift(t, d, cutoff=0):
"""Add ``d`` to every free index (those >= ``cutoff``)."""
if t[0] == "var":
return ("var", t[1] + d) if t[1] >= cutoff else t
if t[0] == "app":
return ("app", shift(t[1], d, cutoff), shift(t[2], d, cutoff))
return ("lam", shift(t[1], d, cutoff + 1))
def subst(t, j, s):
"""Capture-avoiding substitution t[s/j]: the shift under λ avoids capture."""
if t[0] == "var":
return s if t[1] == j else t
if t[0] == "app":
return ("app", subst(t[1], j, s), subst(t[2], j, s))
return ("lam", subst(t[1], j + 1, shift(s, 1)))
def beta(body, arg):
"""Contract the redex (λ. body) arg."""
return shift(subst(body, 0, shift(arg, 1)), -1)
def step_normal(t):
"""One normal-order step (leftmost-outermost redex), or None if normal."""
if t[0] == "var":
return None
if t[0] == "lam":
b = step_normal(t[1])
return None if b is None else ("lam", b)
f, a = t[1], t[2]
if f[0] == "lam":
return beta(f[1], a)
f2 = step_normal(f)
if f2 is not None:
return ("app", f2, a)
a2 = step_normal(a)
return None if a2 is None else ("app", f, a2)
def step_applicative(t):
"""One applicative-order step: reduce inside the function and argument
first, and contract a redex only when both parts are already normal."""
if t[0] == "var":
return None
if t[0] == "lam":
b = step_applicative(t[1])
return None if b is None else ("lam", b)
f, a = t[1], t[2]
f2 = step_applicative(f)
if f2 is not None:
return ("app", f2, a)
a2 = step_applicative(a)
if a2 is not None:
return ("app", f, a2)
return beta(f[1], a) if f[0] == "lam" else None
def normalize(t, fuel=10_000, step=step_normal):
"""Reduce until normal; return (normal form or None, steps used)."""
for n in range(fuel):
nxt = step(t)
if nxt is None:
return t, n
t = nxt
return None, fuel
def church(n):
"""The Church numeral λf. λx. f^n x."""
body = ("var", 0)
for _ in range(n):
body = ("app", ("var", 1), body)
return ("lam", ("lam", body))
def decode_church(t):
"""Return n if ``t`` is exactly the Church numeral n, else None."""
if t is None or t[0] != "lam" or t[1][0] != "lam":
return None
body, n = t[1][1], 0
while body[0] == "app" and body[1] == ("var", 1):
body, n = body[2], n + 1
return n if body == ("var", 0) else None
def standard_defs():
"""Church encodings used in the lesson, as parsed closed terms."""
d = {"ZERO": church(0), "ONE": church(1)}
for name, src in [
("TRUE", r"\x. \y. x"), ("FALSE", r"\x. \y. y"),
("AND", r"\p. \q. p q p"), ("OR", r"\p. \q. p p q"),
("NOT", r"\p. p FALSE TRUE"),
("PAIR", r"\a. \b. \f. f a b"),
("FST", r"\p. p TRUE"), ("SND", r"\p. p FALSE"),
("SUCC", r"\n. \f. \x. f (n f x)"),
("PLUS", r"\m. \n. \f. \x. m f (n f x)"),
("TIMES", r"\m. \n. \f. m (n f)"),
("EXP", r"\m. \n. n m"),
("ISZERO", r"\n. n (\z. FALSE) TRUE"),
("PRED", r"\n. FST (n (\p. PAIR (SND p) (SUCC (SND p))) "
r"(PAIR ZERO ZERO))"),
("Y", r"\f. (\x. f (x x)) (\x. f (x x))"),
("FACT", r"Y (\r. \n. ISZERO n ONE (TIMES n (r (PRED n))))"),
("OMEGA", r"(\x. x x) (\x. x x)"),
("I", r"\x. x"), ("K", r"\x. \y. x"),
]:
d[name] = parse(src, d)
for n in range(10):
d[str(n)] = church(n)
return d
def run(src, defs, step=step_normal, fuel=10_000):
"""Parse ``src`` with ``defs`` and normalize; return (term, steps)."""
return normalize(parse(src, defs), fuel, step)
def main():
d = standard_defs()
print("Normal-order reduction of Church encodings (exact beta-steps)")
for src in ["PLUS 2 3", "TIMES 2 3", "EXP 2 3", "PRED 3", "FACT 3"]:
nf, steps = run(src, d)
print(f" {src:<10} -> Church numeral {decode_church(nf)}"
f" ({steps} steps)")
print()
print("Booleans: AND p q = p q p, OR p q = p p q")
names = {d["TRUE"]: "TRUE", d["FALSE"]: "FALSE"}
for p in ("TRUE", "FALSE"):
for q in ("TRUE", "FALSE"):
a, _ = run(f"AND {p} {q}", d)
o, _ = run(f"OR {p} {q}", d)
print(f" p = {p:<5} q = {q:<5} AND -> {names[a]:<5}"
f" OR -> {names[o]}")
print()
omega = d["OMEGA"]
nf, steps = normalize(omega, 1000)
print("Omega = (λx. x x)(λx. x x)")
print(f" one step turns Omega into Omega again: {step_normal(omega) == omega}")
print(f" normal form within {steps} steps: {nf is not None}")
nf, steps = run("K I OMEGA", d)
print("K I Omega")
print(f" normal order: normal form I after {steps} steps:"
f" {nf == d['I']}")
nf, steps = run("K I OMEGA", d, step=step_applicative, fuel=1000)
print(f" applicative order: normal form within {steps} steps:"
f" {nf is not None}")
print()
print("Epistemic status: exact computations on these particular terms, "
"checked\nby an interpreter. They illustrate, but do not prove, "
"that Church\nnumerals compute arithmetic in general, that Omega "
"has no normal form\n(proved in the lesson and in Lean) or that "
"normal order always finds a\nnormal form when one exists (the "
"standardisation theorem).")
if __name__ == "__main__":
main()What to notice: takes 6 steps and takes 7, the same counts as the Lean evaluator. Kleene's predecessor takes 36 steps for , and the factorial via needs 1477 steps for : Church numerals are a proof of concept, not an efficient representation. normalises in 2 steps in normal order and never in applicative order.
The second experiment tests the Church–Rosser theorem on every term with at most one free variable and at most 10 nodes (variables, applications and abstractions) — 38,438 terms. For each it builds the complete reduction graph, every redex in every order, and checks that any two reducts have a common reduct and that no term reaches two normal forms.
"""Testing the Church-Rosser theorem on every small λ-term.
We enumerate every λ-term (de Bruijn indices, at most one free variable) up
to a given size, build its complete β-reduction graph (all redexes, all
orders) when that graph is small, and check two consequences of confluence:
every two reducts of a term have a common reduct, and no term reaches two
different normal forms. We also count terms that have a normal form but also
an infinite reduction (weakly but not strongly normalising, like K I Omega),
and compare the shortest and longest routes to a normal form. This is
exhaustive evidence for finitely many terms; the theorem is proved in the
lesson and in Lean.
"""
from collections import deque
# Terms: ("var", i), ("app", s, t), ("lam", t).
def terms_of_size(size, depth, free=1, _memo=None):
"""All terms of exactly ``size`` nodes whose indices are < depth + free."""
memo = {} if _memo is None else _memo
key = (size, depth)
if key in memo:
return memo[key]
out = []
if size == 1:
out = [("var", i) for i in range(depth + free)]
elif size >= 2:
out += [("lam", t) for t in terms_of_size(size - 1, depth + 1, free, memo)]
for left in range(1, size - 1):
for s in terms_of_size(left, depth, free, memo):
for t in terms_of_size(size - 1 - left, depth, free, memo):
out.append(("app", s, t))
memo[key] = out
return out
def shift(t, d, cutoff=0):
"""Add ``d`` to every free index (those >= ``cutoff``)."""
if t[0] == "var":
return ("var", t[1] + d) if t[1] >= cutoff else t
if t[0] == "app":
return ("app", shift(t[1], d, cutoff), shift(t[2], d, cutoff))
return ("lam", shift(t[1], d, cutoff + 1))
def subst(t, j, s):
"""Capture-avoiding substitution t[s/j]."""
if t[0] == "var":
return s if t[1] == j else t
if t[0] == "app":
return ("app", subst(t[1], j, s), subst(t[2], j, s))
return ("lam", subst(t[1], j + 1, shift(s, 1)))
def reducts(t, _memo={}):
"""The set of all one-step β-reducts of ``t`` (every redex position)."""
if t in _memo:
return _memo[t]
out = set()
if t[0] == "lam":
out = {("lam", b) for b in reducts(t[1])}
elif t[0] == "app":
f, a = t[1], t[2]
if f[0] == "lam":
out.add(shift(subst(f[1], 0, shift(a, 1)), -1))
out |= {("app", f2, a) for f2 in reducts(f)}
out |= {("app", f, a2) for a2 in reducts(a)}
_memo[t] = frozenset(out)
return _memo[t]
def size_of(t):
"""Number of nodes of a term."""
if t[0] == "var":
return 1
if t[0] == "lam":
return 1 + size_of(t[1])
return 1 + size_of(t[1]) + size_of(t[2])
def reduction_graph(t, cap=200):
"""All terms reachable from ``t`` with their reducts, or None if more
than ``cap`` terms are reachable."""
graph, queue = {}, deque([t])
while queue:
u = queue.popleft()
if u in graph:
continue
graph[u] = reducts(u)
if len(graph) > cap:
return None
queue.extend(v for v in graph[u] if v not in graph)
return graph
def reachable(graph, start):
"""Terms reachable from ``start`` (including itself) in a finite graph."""
seen, stack = {start}, [start]
while stack:
for v in graph[stack.pop()]:
if v not in seen:
seen.add(v)
stack.append(v)
return seen
def has_cycle(graph):
"""Whether the finite reduction graph contains a cycle."""
colour = {}
for root in graph:
if root in colour:
continue
colour[root] = 1
stack = [(root, iter(graph[root]))]
while stack:
node, it = stack[-1]
nxt = next(it, None)
if nxt is None:
colour[node] = 2
stack.pop()
elif colour.get(nxt) == 1:
return True
elif nxt not in colour:
colour[nxt] = 1
stack.append((nxt, iter(graph[nxt])))
return False
def path_lengths(graph, start, target):
"""Shortest and longest path lengths from start to target (acyclic)."""
best = {}
def longest(u):
if u == target:
return 0
if u not in best:
options = [longest(v) for v in graph[u]]
best[u] = 1 + max(x for x in options if x is not None) \
if any(x is not None for x in options) else None
return best[u]
dist, queue = {start: 0}, deque([start])
while queue:
u = queue.popleft()
for v in graph[u]:
if v not in dist:
dist[v] = dist[u] + 1
queue.append(v)
return dist.get(target), longest(start)
def analyse(t, cap=200):
"""Classify one term: returns a dict of facts about its reduction graph."""
graph = reduction_graph(t, cap)
if graph is None:
return {"explored": False}
normal = [u for u in graph if not graph[u]]
reach = {u: reachable(graph, u) for u in graph}
locally_confluent = all(
reach[b] & reach[c]
for u in graph for b in graph[u] for c in graph[u])
return {"explored": True, "normal_forms": len(normal),
"joinable": locally_confluent, "cycle": has_cycle(graph),
"graph_size": len(graph)}
def survey(max_size=8, cap=200):
"""Analyse every term with at most one free variable up to ``max_size``."""
stats = {"terms": 0, "explored": 0, "with_nf": 0, "two_nf": 0,
"not_joinable": 0, "wn_not_sn": 0, "no_nf": 0, "largest": 0}
memo = {}
for size in range(1, max_size + 1):
for t in terms_of_size(size, 0, 1, memo):
stats["terms"] += 1
facts = analyse(t, cap)
if not facts["explored"]:
continue
stats["explored"] += 1
stats["largest"] = max(stats["largest"], facts["graph_size"])
stats["with_nf"] += facts["normal_forms"] >= 1
stats["no_nf"] += facts["normal_forms"] == 0
stats["two_nf"] += facts["normal_forms"] >= 2
stats["not_joinable"] += not facts["joinable"]
stats["wn_not_sn"] += facts["normal_forms"] >= 1 and facts["cycle"]
return stats
NAMED = {
"I": ("lam", ("var", 0)),
"K": ("lam", ("lam", ("var", 1))),
"omega": ("lam", ("app", ("var", 0), ("var", 0))),
"omega3": ("lam", ("app", ("app", ("var", 0), ("var", 0)), ("var", 0))),
}
def examples():
"""Named terms illustrating the possible shapes of reduction graphs."""
i, k, w, w3 = (NAMED[n] for n in ("I", "K", "omega", "omega3"))
z = ("var", 0)
return [
("(λx. x x)((λy. y) z)", ("app", w, ("app", i, z))),
("K I Omega", ("app", ("app", k, i), ("app", w, w))),
("Omega", ("app", w, w)),
("(λx. x x x)(λx. x x x)", ("app", w3, w3)),
]
def main():
max_size = 10
s = survey(max_size)
print(f"All {s['terms']} λ-terms of size <= {max_size} with at most one"
" free variable")
print(f" reduction graph built (<= 200 terms): {s['explored']}")
print(f" largest graph built: {s['largest']}")
print(f" with a normal form: {s['with_nf']}")
print(f" with no normal form: {s['no_nf']}")
print(f" reaching two different normal forms: {s['two_nf']}")
print(f" with two reducts that cannot be joined: {s['not_joinable']}")
print()
print("Some larger terms:")
for name, t in examples():
g = reduction_graph(t, cap=200)
if g is None:
print(f" {name:<24} more than 200 reducts: the graph keeps growing")
continue
nfs = [u for u in g if not g[u]]
terms = f"{len(g)} term" + ("s" if len(g) != 1 else "")
count = {0: "no normal form", 1: "one normal form"}.get(
len(nfs), f"{len(nfs)} normal forms")
line = f" {name:<24} {terms}, {count}"
if nfs and has_cycle(g):
line += ", also an infinite reduction"
elif nfs:
short, long_ = path_lengths(g, t, nfs[0])
line += f", routes of {short} to {long_} steps"
else:
line += ", every reduction is infinite"
print(line)
print()
print("Epistemic status: an exhaustive check of finitely many terms (and "
"only\nof those whose reduction graphs are small). It is evidence "
"for the\nChurch-Rosser theorem, not a proof: the proof, by "
"parallel reduction,\nis in the lesson and is machine-checked in "
"Lean.")
if __name__ == "__main__":
main()No counterexample, as the theorem guarantees. Notice : different routes to its normal form take 2 or 3 steps, so the answer is independent of the strategy but the cost is not. And has an ever-growing reduction graph. Both experiments are evidence for finitely many terms; the theorems are proved above and in Lean.
Explore: reduction graphs and Church booleans
Here is the full reduction graph of with . Each arrow contracts one redex. Follow any path from the top: all of them end at .
The parallel-reduction proof fills every such picture with diamonds: from , any two parallel reductions meet again at the complete development in one parallel step each.
Church booleans are selectors, so means "if then else ". Is that really "and"? Compare it with in the truth table, writing "if then else " as . Then try , "if then else ".
Church's AND, read as if-then-else
AND p q = p q p means ‘if p then q else p’.
Read as: (p ∧ q) ∨ (¬p ∧ p)
Read as: p ∧ q
Use the symbols ¬ ∧ ∨ → ↔, the words not, and, or, implies, iff, or ASCII ~ & | -> <->. Variables are single lowercase letters. ¬ binds tightest, then ∧, ∨, →, ↔; p → q → r means p → (q → r).
Contingent — true in 1 of 4 rows, false in the others.
Equivalent — the two formulas agree in all 4 rows.
| p | q | p ∧ q | ¬p | ¬p ∧ p | p ∧ q ∨ ¬p ∧ p | p ∧ q | Rows where they differ |
|---|---|---|---|---|---|---|---|
| T | T | T | F | F | T | T | |
| T | F | F | F | F | F | F | |
| F | T | F | T | F | F | F | |
| F | F | F | T | F | F | F |
4 rows: one for every way of making the 2 variables true or false.
Exercises
1. The λ-calculus
Warm-upHumanList the free variables of the term . Enter each variable name once.
2. The λ-calculus
Warm-upHumanWhat does reduce to in one β-step (with free)?
3. The λ-calculus
CoreHumanWith Church numerals , the term (the numeral applied to the numeral ) β-reduces to a Church numeral . What is ?
4. The λ-calculus
CoreHumanWhich of these terms have no normal form? Here , , . Choose all that apply.
5. The λ-calculus
CoreHumanWhy does the proof of the Church–Rosser theorem go through parallel reduction instead of showing directly that one-step reduction has the diamond property?
6. The λ-calculus
StretchHumanIn Turing's proof of weak normalisation we contract a redex of maximal degree chosen so that contains no redex of degree . Which part of the argument needs this choice?
7. The λ-calculus
CoreHumanWhich of these terms has a type in the simply typed λ-calculus (for suitable types on the bound variables)?
8. The λ-calculus
CoreFormalIn the lesson's Lean file, Step t u is one β-step and ReflTransGen Step t u is . Which statement is the Church–Rosser theorem, as proved there?
9. The λ-calculus
CoreComputationalPython's lambda can run Church numerals directly. With church(n) and to_int below, define times(m, n) and exp(m, n) returning the Church numerals for and , using only application and lambda (no conversion to int inside them).
Common misconceptions
Misconception
Substituting without renaming bound variables. is not : the free would be captured. Rename the bound variable first: . De Bruijn indices make the problem disappear, which is why proof assistants use them.
Misconception
"Every λ-term has a normal form." reduces only to itself, so it has none (proved in Lean above); so does , since reduction also happens under . Only typed calculi guarantee normal forms.
Misconception
"If a term has a normal form, every reduction strategy finds it." has normal form , but reducing first loops forever. Church–Rosser says the normal form is unique; standardisation says normal order finds it. Neither says every strategy terminates.
Misconception
"Church–Rosser means reduction terminates." Confluence is about agreement, not termination: is confluent (all its reducts are ) and never terminates. Uniqueness and existence of normal forms are separate questions.
Misconception
" means one reduces to the other." β-equality allows reductions in both directions. , but neither reduces to the other; they have a common reduct (Corollary 3 says this is always the case for β-equal terms).
Viewpoints
Computation— Evaluation strategies and functional programming
To a programmer, the λ-calculus is the core of every functional language, and the strategies of this lesson are real design choices. Most languages (Python, OCaml, Scheme) use call-by-value: evaluate arguments first — applicative order, which loops on but never duplicates work. Haskell uses call-by-need, normal order with sharing, so that a duplicated argument such as the above is evaluated at most once. In a call-by-value language loops even when used correctly; one uses the η-expanded instead. Closures, currying ( as a function returning a function) and higher-order functions such as map all come from here. Peter Landin's SECD machine (1964) was the first abstract machine for evaluating λ-terms.
Logic— A failed logic and a successful calculus
Church introduced λ-notation as part of a system of logic intended as a foundation of mathematics. In 1935 Kleene and Rosser showed that system inconsistent, and Curry later distilled the problem into a short paradox built on the same self-application as : with a fixed point of , every proposition becomes provable. What survived was the pure calculus of functions, whose consistency as an equational theory is exactly Corollary 3 (). Church then used it to give the first published undecidable problem (1936), and with types it became a logic again: under the Curry–Howard correspondence, simply typed terms are proofs in intuitionistic propositional logic and β-reduction is the elimination of detours in proofs. See type theory and proof theory.
Topology— Scott's models: continuity tames self-application
Can the untyped calculus be modelled by actual functions? Self-application suggests a set with , which Cantor's theorem forbids unless . In 1969 Dana Scott found the way out: work with complete lattices and only the continuous functions for the Scott topology (the open sets are the upward-closed sets that cannot be reached by a directed supremum from outside). He constructed a lattice , as an inverse limit, with , the space of continuous functions — a non-trivial model in which every λ-term denotes an element and β-equal terms denote the same element. The Scott topology is but not Hausdorff: its specialisation order is the information order, " when is at least as defined as ". Continuity — a finite part of the output depends only on a finite part of the input — is a topological statement about computation, and domain theory grew from it.
Algebra— Combinators and combinatory algebras
Bound variables can be eliminated entirely. In combinatory logic (Schönfinkel 1924, Curry 1930) there are just two constants with the rules and , and every λ-term can be translated into an expression in and by "bracket abstraction" (for instance , as computed above). Algebraically, a combinatory algebra is a set with a binary operation (application) containing elements obeying these two laws; it is combinatorially complete: every polynomial in the operation is represented by an element. Models of the λ-calculus are combinatory algebras satisfying a few extra axioms (λ-algebras), and the λ-calculus becomes an equational algebraic theory — though a strange one, since application is neither associative nor commutative.
Category theory— Cartesian closed categories
Joachim Lambek observed (around 1980) that the simply typed λ-calculus with products is the internal language of cartesian closed categories: types are objects, a term is an arrow , abstraction is the bijection , and β- and η-equality are the two triangle laws of that adjunction. Untyped models live in a CCC as reflexive objects with a retract of — Scott's is one in the category of complete lattices and continuous maps. See categorical logic.
History and global context
- Moses Schönfinkel (working in Göttingen) introduced combinators in "Über die Bausteine der mathematischen Logik" (1924); Haskell Curry developed combinatory logic from his 1930 dissertation onwards.
- Alonzo Church introduced λ-notation in "A set of postulates for the foundation of logic" (Annals of Mathematics, 1932–33). Stephen Kleene and J. Barkley Rosser showed the logical system inconsistent (1935). Kleene, then Church's student, found the λ-definition of the predecessor; he later recalled that the idea came to him at the dentist's.
- Church and Rosser proved confluence in "Some properties of conversion" (Transactions of the AMS, 1936). Church's "An unsolvable problem of elementary number theory" (1936) proposed identifying effective computability with λ-definability and gave the first undecidability result; Kleene (1936) and Alan Turing ("Computability and λ-definability", 1937) proved the equivalences with recursive functions and Turing machines.
- Church's simple theory of types (1940) introduced the typed calculus. Turing's normalisation argument was published by Robin Gandy in 1980; William Tait proved strong normalisation in 1967. The parallel-reduction proof of Church–Rosser is due to Tait and Per Martin-Löf, and the complete-development form to Masako Takahashi (1995). N. G. de Bruijn introduced nameless indices for the Automath proof checker (1972).
- John McCarthy's LISP (1960) borrowed λ-notation; Peter Landin (1964–66) showed how to understand programming languages through the λ-calculus; Corrado Böhm proved his separation theorem (1968); Dana Scott built the first mathematical models (1969). Henk Barendregt's monograph (1981; revised 1984) made the subject a branch of mathematics.
Further reading
- Henk Barendregt, The Lambda Calculus: Its Syntax and Semantics (1981; revised edition 1984) — the standard reference, including standardisation and Böhm's theorem.
- J. Roger Hindley and Jonathan P. Seldin, Lambda-Calculus and Combinators: An Introduction (2008) — a readable first course, with combinatory logic and types.
- Benjamin C. Pierce, Types and Programming Languages (2002) — the λ-calculus from the programming-language side, with implementations.
- Jean-Yves Girard, Yves Lafont and Paul Taylor, Proofs and Types (1989) — normalisation proofs and the Curry–Howard correspondence.
- Morten Heine Sørensen and Paweł Urzyczyn, Lectures on the Curry–Howard Isomorphism (2006) — untyped and typed calculi, with full proofs.
- Masako Takahashi, "Parallel reductions in λ-calculus", Information and Computation 118 (1995) — the triangle-property proof used here.