2. Named Terms
import LeanLambda.Untyped.DeBruijnnamespace Untyped2.1. Named Syntax
We now return to the notation used at the beginning of the chapter. De Bruijn
indices are convenient for the implementation, but a term such as
λ λ 1 0 is unnecessarily difficult to read. Named terms record the
syntax written on paper: variables have names, and lambdas say which name they
bind.
e ::= x \mid \lambda x.\ e \mid e\ e
This datatype is raw syntax: it remembers the literal choice of binder name.
Consequently λx. x and λy. y are different values of
Term, even though they describe the same function. We will recover the
usual notion of equality up to binder renaming through compilation.
inductive Term where
| var (x : String)
| lam (x : String) (body : Term)
| app (fn arg : Term)
deriving DecidableEq, Repr, Lean.ToExprsyntax_rules namedTerm quoted_by "Term[" namedTerm "]" where
| "{" t:term "}" => t
| "(" t:namedTerm ")" => parse t
| x:ident => Term.var x
| "λ" x:ident "." body:namedTerm => Term.lam x (parse body)
| "λ" "{" x:term "}" "." body:namedTerm => Term.lam x (parse body)
| f:namedTerm:70 a:namedTerm:71 => Term.app (parse f) (parse a)example : Term[λx. x] = Term.lam "x" (Term.var "x") := ⊢ Term.lam "x" (Term.var "x") = Term.lam "x" (Term.var "x") All goals completed! 🐙example : Term[x y z] = Term[(x y) z] := ⊢ ((Term.var "x").app (Term.var "y")).app (Term.var "z") = ((Term.var "x").app (Term.var "y")).app (Term.var "z") All goals completed! 🐙2.2. Compilation to De Bruijn Terms
Compilation translates readable named syntax into the de Bruijn representation
that already carries the semantics. While traversing a term, the list
ctx records the binders currently in scope. The nearest binder is at the
head.
When the compiler encounters a variable x, it searches this list. If
the first occurrence of x is at position n, the variable becomes
bound index n. If x is absent, it remains a named free variable.
\begin{array}{rcl}
\llbracket x \rrbracket_\rho
&=& \begin{cases}
n & \rho(n) = x\\
x & x \notin \rho
\end{cases}\\
\llbracket \lambda x.\ e \rrbracket_\rho
&=& \lambda.\ \llbracket e \rrbracket_{x,\rho}\\
\llbracket e_1\ e_2 \rrbracket_\rho
&=& \llbracket e_1 \rrbracket_\rho\ \llbracket e_2 \rrbracket_\rho
\end{array}
For example, compiling λx. λy. x proceeds with the binder lists
[], then [x], then [y,x]. The occurrence of x is
found at position one, producing λ λ 1.
Adding new binders at the head also handles shadowing. In
λx. λx. x, the occurrence is found at position zero and therefore
refers to the inner lambda.
namespace Term@[simp] def toDBWith (ctx : List String) : Term -> DB
| .var x =>
(ctx.idxOf? x).elim (.free x) (.bound)
| .lam x body =>
.lam (toDBWith (x :: ctx) body)
| .app fn arg =>
.app (toDBWith ctx fn) (toDBWith ctx arg)abbrev toDB (t : Term) : DB := toDBWith [] texample : toDB Term[λx. x] = DB[λ 0] := ⊢ (lam "x" (var "x")).toDB = (DB.bound 0).lam All goals completed! 🐙example : toDB Term[λx. λy. x] = DB[λ λ 1] := ⊢ (lam "x" (lam "y" (var "x"))).toDB = (DB.bound 1).lam.lam All goals completed! 🐙example : toDB Term[λx. λx. x] = DB[λ λ 0] := ⊢ (lam "x" (lam "x" (var "x"))).toDB = (DB.bound 0).lam.lam All goals completed! 🐙example : toDB Term[λx. x y] = DB[λ 0 y] := ⊢ (lam "x" ((var "x").app (var "y"))).toDB = ((DB.bound 0).app (DB.free "y")).lam All goals completed! 🐙
The compiler cannot create a dangling index: every index comes from an actual
name in the binder list. The first theorem states the precise invariant. If
the list contains d binders, the compiled term is scoped at depth
d. Starting with the empty list therefore produces a locally closed
term.
@[grind .] theorem toDBWith_scoped
: DB.ScopedAt ρ.length (toDBWith ρ e)
:= ρ:List Stringe:Term⊢ DB.ScopedAt ρ.length (toDBWith ρ e) induction e generalizing ρ with
x:Stringρ:List String⊢ DB.ScopedAt ρ.length (toDBWith ρ (var x)) x:Stringρ:List Stringh:List.idxOf? x ρ = none⊢ DB.ScopedAt ρ.length (toDBWith ρ (var x))x:Stringρ:List Stringval✝:ℕh:List.idxOf? x ρ = some val✝⊢ DB.ScopedAt ρ.length (toDBWith ρ (var x)) x:Stringρ:List Stringh:List.idxOf? x ρ = none⊢ DB.ScopedAt ρ.length (toDBWith ρ (var x))x:Stringρ:List Stringval✝:ℕh:List.idxOf? x ρ = some val✝⊢ DB.ScopedAt ρ.length (toDBWith ρ (var x)) All goals completed! 🐙
fn✝:Termarg✝:Termfn_ih✝:∀ {ρ : List String}, DB.ScopedAt ρ.length (toDBWith ρ fn✝)arg_ih✝:∀ {ρ : List String}, DB.ScopedAt ρ.length (toDBWith ρ arg✝)ρ:List String⊢ DB.ScopedAt ρ.length (toDBWith ρ (fn✝.app arg✝))x✝:Stringbody✝:Termbody_ih✝:∀ {ρ : List String}, DB.ScopedAt ρ.length (toDBWith ρ body✝)ρ:List String⊢ DB.ScopedAt ρ.length (toDBWith ρ (lam x✝ body✝)) All goals completed! 🐙@[grind .] theorem toDB_locallyClosed
: DB.LocallyClosed (toDB e)
:= e:Term⊢ e.toDB.LocallyClosed All goals completed! 🐙To recover an actual named common reduct later, we also need to print a scoped de Bruijn term with names. Binder names are chosen fresh from the names already in use; their particular spelling has no mathematical significance.
theorem exists_toDBWith
(hscoped : DB.ScopedAt ctx.length t)
(hnodup : ctx.Nodup)
(hfree : ∀ x ∈ DB.FV t, x ∉ ctx)
: ∃ e, toDBWith ctx e = t
:= t:DBctx:List Stringhscoped:DB.ScopedAt ctx.length thnodup:ctx.Noduphfree:∀ x ∈ t.FV, x ∉ ctx⊢ ∃ e, toDBWith ctx e = t
induction t generalizing ctx with
n:ℕctx:List Stringhscoped:DB.ScopedAt ctx.length (DB.bound n)hnodup:ctx.Noduphfree:∀ x ∈ (DB.bound n).FV, x ∉ ctx⊢ ∃ e, toDBWith ctx e = DB.bound n
n:ℕctx:List Stringhscoped:DB.ScopedAt ctx.length (DB.bound n)hnodup:ctx.Noduphfree:∀ x ∈ (DB.bound n).FV, x ∉ ctx⊢ toDBWith ctx (var ctx[n]) = DB.bound n; All goals completed! 🐙
x:Stringctx:List Stringhscoped:DB.ScopedAt ctx.length (DB.free x)hnodup:ctx.Noduphfree:∀ x_1 ∈ (DB.free x).FV, x_1 ∉ ctx⊢ ∃ e, toDBWith ctx e = DB.free x
x:Stringctx:List Stringhscoped:DB.ScopedAt ctx.length (DB.free x)hnodup:ctx.Noduphfree:∀ x_1 ∈ (DB.free x).FV, x_1 ∉ ctx⊢ toDBWith ctx (var x) = DB.free x; All goals completed! 🐙
body:DBih:∀ {ctx : List String}, DB.ScopedAt ctx.length body → ctx.Nodup → (∀ x ∈ body.FV, x ∉ ctx) → ∃ e, toDBWith ctx e = bodyctx:List Stringhscoped:DB.ScopedAt ctx.length body.lamhnodup:ctx.Noduphfree:∀ x ∈ body.lam.FV, x ∉ ctx⊢ ∃ e, toDBWith ctx e = body.lam
body:DBih:∀ {ctx : List String}, DB.ScopedAt ctx.length body → ctx.Nodup → (∀ x ∈ body.FV, x ∉ ctx) → ∃ e, toDBWith ctx e = bodyctx:List Stringhscoped:DB.ScopedAt ctx.length body.lamhnodup:ctx.Noduphfree:∀ x ∈ body.lam.FV, x ∉ ctxx:String := freshString (ctx.toFinset ∪ body.FV)⊢ ∃ e, toDBWith ctx e = body.lam
obtain_by namedBody : toDBWith (x :: ctx) namedBody = body := body:DBih:∀ {ctx : List String}, DB.ScopedAt ctx.length body → ctx.Nodup → (∀ x ∈ body.FV, x ∉ ctx) → ∃ e, toDBWith ctx e = bodyctx:List Stringhscoped:DB.ScopedAt ctx.length body.lamhnodup:ctx.Noduphfree:∀ x ∈ body.lam.FV, x ∉ ctxx:String := freshString (ctx.toFinset ∪ body.FV)⊢ ∃ namedBody, toDBWith (x :: ctx) namedBody = body
All goals completed! 🐙
body:DBih:∀ {ctx : List String}, DB.ScopedAt ctx.length body → ctx.Nodup → (∀ x ∈ body.FV, x ∉ ctx) → ∃ e, toDBWith ctx e = bodyctx:List Stringhscoped:DB.ScopedAt ctx.length body.lamhnodup:ctx.Noduphfree:∀ x ∈ body.lam.FV, x ∉ ctxx:String := freshString (ctx.toFinset ∪ body.FV)namedBody:Termh✝:toDBWith (x :: ctx) namedBody = body⊢ toDBWith ctx (lam x namedBody) = body.lam; All goals completed! 🐙
fn:DBarg:DBihfn:∀ {ctx : List String}, DB.ScopedAt ctx.length fn → ctx.Nodup → (∀ x ∈ fn.FV, x ∉ ctx) → ∃ e, toDBWith ctx e = fniharg:∀ {ctx : List String}, DB.ScopedAt ctx.length arg → ctx.Nodup → (∀ x ∈ arg.FV, x ∉ ctx) → ∃ e, toDBWith ctx e = argctx:List Stringhscoped:DB.ScopedAt ctx.length (fn.app arg)hnodup:ctx.Noduphfree:∀ x ∈ (fn.app arg).FV, x ∉ ctx⊢ ∃ e, toDBWith ctx e = fn.app arg
obtain_by namedFn : toDBWith ctx namedFn = fn := fn:DBarg:DBihfn:∀ {ctx : List String}, DB.ScopedAt ctx.length fn → ctx.Nodup → (∀ x ∈ fn.FV, x ∉ ctx) → ∃ e, toDBWith ctx e = fniharg:∀ {ctx : List String}, DB.ScopedAt ctx.length arg → ctx.Nodup → (∀ x ∈ arg.FV, x ∉ ctx) → ∃ e, toDBWith ctx e = argctx:List Stringhscoped:DB.ScopedAt ctx.length (fn.app arg)hnodup:ctx.Noduphfree:∀ x ∈ (fn.app arg).FV, x ∉ ctx⊢ ∃ namedFn, toDBWith ctx namedFn = fn
All goals completed! 🐙
obtain_by namedArg : toDBWith ctx namedArg = arg := fn:DBarg:DBihfn:∀ {ctx : List String}, DB.ScopedAt ctx.length fn → ctx.Nodup → (∀ x ∈ fn.FV, x ∉ ctx) → ∃ e, toDBWith ctx e = fniharg:∀ {ctx : List String}, DB.ScopedAt ctx.length arg → ctx.Nodup → (∀ x ∈ arg.FV, x ∉ ctx) → ∃ e, toDBWith ctx e = argctx:List Stringhscoped:DB.ScopedAt ctx.length (fn.app arg)hnodup:ctx.Noduphfree:∀ x ∈ (fn.app arg).FV, x ∉ ctxnamedFn:Termh✝:toDBWith ctx namedFn = fn⊢ ∃ namedArg, toDBWith ctx namedArg = arg
All goals completed! 🐙
fn:DBarg:DBihfn:∀ {ctx : List String}, DB.ScopedAt ctx.length fn → ctx.Nodup → (∀ x ∈ fn.FV, x ∉ ctx) → ∃ e, toDBWith ctx e = fniharg:∀ {ctx : List String}, DB.ScopedAt ctx.length arg → ctx.Nodup → (∀ x ∈ arg.FV, x ∉ ctx) → ∃ e, toDBWith ctx e = argctx:List Stringhscoped:DB.ScopedAt ctx.length (fn.app arg)hnodup:ctx.Noduphfree:∀ x ∈ (fn.app arg).FV, x ∉ ctxnamedFn:Termh✝¹:toDBWith ctx namedFn = fnnamedArg:Termh✝:toDBWith ctx namedArg = arg⊢ toDBWith ctx (namedFn.app namedArg) = fn.app arg; All goals completed! 🐙theorem exists_toDB_of_locallyClosed
: DB.LocallyClosed t
→ ∃ e, toDB e = t
:= t:DB⊢ t.LocallyClosed → ∃ e, e.toDB = t All goals completed! 🐙2.3. Free Variables of Named Terms
Free variables are easy to read directly from named syntax, so here we use the standard recursive definition rather than hiding it behind compilation. A variable contributes its name, application takes the union, and a lambda removes the name it binds.
\begin{array}{rcl}
FV(x) &=& \{x\}\\
FV(\lambda x.\ e) &=& FV(e) \setminus \{x\}\\
FV(e_1\ e_2) &=& FV(e_1) \cup FV(e_2)
\end{array}
This does not introduce a competing meaning of “free variable.” The theorem
fv_toDBWith proves that the direct definition agrees with the de Bruijn
one after compilation. Its extra condition z ∉ ρ says that a name
already supplied by the binder list is no longer free. With an empty binder
list this condition disappears, giving fv_toDB.
A named term is closed when this set is empty. Abstraction shape is another
simple syntactic observation, so IsAbstraction is also defined directly
on Term. The bridge theorems below show that closedness and abstraction
shape agree with compilation.
@[simp] def FV : Term -> Set String
| .var x => {y | y = x}
| .lam x body => {y | y ∈ FV body ∧ y ≠ x}
| .app fn arg => {y | y ∈ FV fn ∨ y ∈ FV arg}def Closed (t : Term) : Prop :=
∀ x, x ∉ FV texample : FV Term[λx. x y] = {"y"} := ⊢ (lam "x" ((var "x").app (var "y"))).FV = {"y"} All goals completed! 🐙example : Closed Term[λx. x] := ⊢ (lam "x" (var "x")).Closed All goals completed! 🐙@[grind =] theorem fv_toDBWith
: z ∈ DB.FV (toDBWith ρ e) ↔ z ∈ FV e ∧ z ∉ ρ
:= ρ:List Stringe:Termz:String⊢ z ∈ (toDBWith ρ e).FV ↔ z ∈ e.FV ∧ z ∉ ρ induction e generalizing ρ with
z:Stringy:Stringρ:List String⊢ z ∈ (toDBWith ρ (var y)).FV ↔ z ∈ (var y).FV ∧ z ∉ ρ z:Stringy:Stringρ:List Stringh:List.idxOf? y ρ = none⊢ z ∈ (toDBWith ρ (var y)).FV ↔ z ∈ (var y).FV ∧ z ∉ ρz:Stringy:Stringρ:List Stringval✝:ℕh:List.idxOf? y ρ = some val✝⊢ z ∈ (toDBWith ρ (var y)).FV ↔ z ∈ (var y).FV ∧ z ∉ ρ z:Stringy:Stringρ:List Stringh:List.idxOf? y ρ = none⊢ z ∈ (toDBWith ρ (var y)).FV ↔ z ∈ (var y).FV ∧ z ∉ ρz:Stringy:Stringρ:List Stringval✝:ℕh:List.idxOf? y ρ = some val✝⊢ z ∈ (toDBWith ρ (var y)).FV ↔ z ∈ (var y).FV ∧ z ∉ ρ All goals completed! 🐙
z:Stringfn✝:Termarg✝:Termfn_ih✝:∀ {ρ : List String}, z ∈ (toDBWith ρ fn✝).FV ↔ z ∈ fn✝.FV ∧ z ∉ ρarg_ih✝:∀ {ρ : List String}, z ∈ (toDBWith ρ arg✝).FV ↔ z ∈ arg✝.FV ∧ z ∉ ρρ:List String⊢ z ∈ (toDBWith ρ (fn✝.app arg✝)).FV ↔ z ∈ (fn✝.app arg✝).FV ∧ z ∉ ρz:Stringx✝:Stringbody✝:Termbody_ih✝:∀ {ρ : List String}, z ∈ (toDBWith ρ body✝).FV ↔ z ∈ body✝.FV ∧ z ∉ ρρ:List String⊢ z ∈ (toDBWith ρ (lam x✝ body✝)).FV ↔ z ∈ (lam x✝ body✝).FV ∧ z ∉ ρ All goals completed! 🐙@[grind =] theorem fv_toDB
: z ∈ FV e ↔ z ∈ DB.FV (toDB e)
:= e:Termz:String⊢ z ∈ e.FV ↔ z ∈ e.toDB.FV All goals completed! 🐙@[grind =] theorem closed_iff_toDB_closed
: Closed e ↔ DB.Closed (toDB e)
:= e:Term⊢ e.Closed ↔ e.toDB.Closed All goals completed! 🐙2.4. Semantics of Named Terms
2.4.1. Alpha-Equivalence
Changing a binder name should not change the term, provided its bound occurrences are changed with it. Thus
\lambda x.\ x =_\alpha \lambda y.\ y
and λx. λy. x is alpha-equivalent to λz. λy. z.
Free variables, however, may not be renamed: λx. y is not
alpha-equivalent to λy. y, because the latter change captures the free
y.
De Bruijn compilation already forgets exactly the irrelevant binder names. We therefore define two named terms to be alpha-equivalent when their compiled terms are literally equal.
e =_\alpha e' \quad\text{iff}\quad
\llbracket e \rrbracket_\cdot = \llbracket e' \rrbracket_\cdot
Most statements use the unsuffixed relation with empty binder lists. The
With version records binder lists explicitly and is needed later when a
theorem is applied underneath surrounding lambdas.
abbrev AlphaEqWith (ρ ρ' : List String) (e e' : Term) : Prop :=
toDBWith ρ e = toDBWith ρ' e'notation:50 e " =α[" ρ ", " ρ' "] " e' => AlphaEqWith ρ ρ' e e'def AlphaEq (s t : Term) : Prop :=
AlphaEqWith [] [] s tinfix:50 " =α " => Term.AlphaEqAlpha-renaming cannot change which variables are free. It therefore also preserves whether a term is closed. Both facts follow immediately by passing through the compiler-coherence theorem for free variables.
@[grind →] theorem AlphaEq.fv_eq
: s =α t → FV s = FV t
:= s:Termt:Term⊢ s =α t → s.FV = t.FV All goals completed! 🐙@[grind →] theorem AlphaEq.closed_iff
: s =α t → (Closed s ↔ Closed t)
:= s:Termt:Term⊢ s =α t → (s.Closed ↔ t.Closed) All goals completed! 🐙
For concrete terms, alpha_eq unfolds compilation and checks whether the
resulting de Bruijn terms are equal.
syntax "alpha_eq" : tacticmacro_rules
| `(tactic| alpha_eq) =>
`(tactic|
simp_all [_root_.Untyped.Term.AlphaEq, _root_.Untyped.Term.AlphaEqWith,
_root_.Untyped.Term.toDB, _root_.Untyped.Term.toDBWith,
List.idxOf?, List.findIdx?, List.findIdx?.go] <;> grind)example : Term[λx. x] =α Term[λy. y] := ⊢ lam "x" (var "x") =α lam "y" (var "y") All goals completed! 🐙example : ¬ Term[λx. y] =α Term[λy. y] := ⊢ ¬lam "x" (var "y") =α lam "y" (var "y") All goals completed! 🐙2.4.2. Beta Reduction
We could now define capture-avoiding substitution a second time for named syntax, including a procedure for choosing fresh names. That would add a large amount of machinery without changing the calculus. Instead, named beta reduction simply means beta reduction after compilation:
e \to_\beta e'
\quad\text{iff}\quad
\llbracket e \rrbracket \to_\beta \llbracket e' \rrbracket.
For example, contracting (λx. λy. x) y is performed on
(λ λ 1) y, whose result is λ y. Any named term compiling to that
result is an acceptable target; the particular choice of a fresh binder name
is irrelevant up to alpha-equivalence.
As before, the unsuffixed relations are the ordinary top-level statements, and
the With relations carry binder lists for use beneath surrounding
lambdas.
abbrev BetaStepWith (ρ ρ' : List String) (e e' : Term) : Prop :=
toDBWith ρ e →βᵇ toDBWith ρ' e'notation:50 e " →β[" ρ ", " ρ' "] " e' => BetaStepWith ρ ρ' e e'def BetaStep (s t : Term) : Prop :=
BetaStepWith [] [] s tinfix:50 " →β " => BetaStepabbrev BetaStarWith (ρ ρ' : List String) (e e' : Term) : Prop :=
(toDBWith ρ e) →βᵇ* (toDBWith ρ' e')notation:50 e " →β*[" ρ ", " ρ' "] " e' => BetaStarWith ρ ρ' e e'def BetaStar (s t : Term) : Prop :=
BetaStarWith [] [] s tinfix:50 " →β* " => BetaStarFor named calculations, the beta tactics unfold compilation and then use the corresponding de Bruijn tactics. The first example contracts an identity; the second shows that compilation handles capture avoidance without choosing a distinguished fresh binder name.
macro_rules
| `(tactic| beta_step) =>
`(tactic|
simp [_root_.Untyped.Term.BetaStep, _root_.Untyped.Term.BetaStepWith,
_root_.Untyped.Term.toDB, _root_.Untyped.Term.toDBWith,
List.idxOf?, List.findIdx?, List.findIdx?.go] <;> repeat' constructor)macro_rules
| `(tactic| beta_steps) =>
`(tactic|
simp [_root_.Untyped.Term.BetaStar, _root_.Untyped.Term.toDB, _root_.Untyped.Term.toDBWith,
_root_.Untyped.Term.BetaStarWith,
List.idxOf?, List.findIdx?, List.findIdx?.go] <;>
first
| exact Relation.ReflTransGen.refl
| apply Relation.ReflTransGen.head
aux_beta_steps)example : Term[(λx. x) y] →β Term[y] := ⊢ (lam "x" (var "x")).app (var "y") →β var "y" All goals completed! 🐙example : Term[(λx. λy. x) y] →β Term[λz. y] := ⊢ (lam "x" (lam "y" (var "x"))).app (var "y") →β lam "z" (var "y") All goals completed! 🐙example : Term[(λx. x) ((λy. y) z)] →β* Term[z] := ⊢ (lam "x" (var "x")).app ((lam "y" (var "y")).app (var "z")) →β* var "z" All goals completed! 🐙2.4.3. Normal and Neutral Terms
A named term is neutral or normal when its compiled term has the corresponding positive shape. A term is reducible when its compiled term has at least one beta step. We quantify the target of reducibility as a de Bruijn term because merely asking whether a step exists should not require us to manufacture fresh binder names for its result.
abbrev NeutralWith (ρ : List String) (e : Term) : Prop :=
DB.Neutral (toDBWith ρ e)abbrev Neutral (e : Term) : Prop :=
NeutralWith [] eabbrev NormalWith (ρ : List String) (e : Term) : Prop :=
DB.Normal (toDBWith ρ e)abbrev Normal (e : Term) : Prop :=
NormalWith [] eabbrev ReducibleWith (ρ: List String) (e : Term) : Prop :=
DB.Reducible (toDBWith ρ e)abbrev Reducible (e : Term) : Prop :=
ReducibleWith [] e
The same normal tactic checks concrete named terms by compiling them.
example : Normal Term[λx. x] := ⊢ (lam "x" (var "x")).Normal All goals completed! 🐙example : Normal Term[x (λy. y)] := ⊢ ((var "x").app (lam "y" (var "y"))).Normal All goals completed! 🐙example : Neutral Term[x (λy. y)] := ⊢ ((var "x").app (lam "y" (var "y"))).Neutral All goals completed! 🐙2.4.3.1. Characterization of Normality
As for de Bruijn terms, the positive shape judgment says exactly that no beta step is possible.
theorem normalWith_iff_not_reducibleWith
: NormalWith ρ t ↔ ¬ ReducibleWith ρ t
:= DB.normal_iff_not_reducibletheorem normal_iff_not_reducible
: Normal t ↔ ¬ Reducible t
:= normalWith_iff_not_reducibleWith2.4.4. Progress and Finality
These are the named, mathematically readable forms of the two fundamental normal-form results. Compilation supplies their proofs, but the statements are about named lambda terms and require no typing assumption.
2.4.4.1. Progress
Progress. Every named term either takes a beta step or is normal.
theorem progress_with
: ReducibleWith ρ e ∨ NormalWith ρ e
:= DB.progresstheorem progress
: Reducible e ∨ Normal e
:= progress_with2.4.4.2. Finality
Finality of normal forms. A normal named term cannot take a beta step.
theorem finality_with
(hnormal : NormalWith ρ e)
: ¬ ReducibleWith ρ e
:= ρ:List Stringe:Termhnormal:NormalWith ρ e⊢ ¬ReducibleWith ρ e All goals completed! 🐙theorem finality
(hnormal : Normal e)
: ¬ Reducible e
:= e:Termhnormal:e.Normal⊢ ¬e.Reducible All goals completed! 🐙Alpha-equivalence, beta reduction, multi-step reduction, abstraction shape, reducibility, and normality are therefore all views of the same de Bruijn semantics. The named layer adds readability, not a second calculus whose substitution theory would have to be developed again.
2.5. Programming with Untyped Terms
The calculus contains no primitive data. Nevertheless, functions can encode familiar computations. From this point onward we use named terms for examples and rely on compilation for their meaning.
The identity function returns its argument. Church booleans encode true and false by their choice between two arguments: true selects the first, while false selects the second. A conditional can therefore apply its boolean to the two branches. Negation asks the boolean to choose false before true, reversing its result.
def I : Term := Term[λx. x]def T : Term := Term[λx. λy. x]def F : Term := Term[λx. λy. y]def N : Term := Term[λb. b {F} {T}]example : Term[{I} x] →β Term[x] := ⊢ I.app (var "x") →β var "x" All goals completed! 🐙example : Term[{T} x y] →β* Term[x] := ⊢ (T.app (var "x")).app (var "y") →β* var "x" calc
_ = Term[(λx. λy. x) x y] := ⊢ (T.app (var "x")).app (var "y") = ((lam "x" (lam "y" (var "x"))).app (var "x")).app (var "y") All goals completed! 🐙
_ →β Term[(λb. x) y] := ⊢ ((lam "x" (lam "y" (var "x"))).app (var "x")).app (var "y") →β (lam "b" (var "x")).app (var "y") All goals completed! 🐙
_ →β Term[x] := ⊢ (lam "b" (var "x")).app (var "y") →β var "x" All goals completed! 🐙example : Term[{F} x y] →β* Term[y] := ⊢ (F.app (var "x")).app (var "y") →β* var "y" All goals completed! 🐙example : Term[{N} {T}] →β* Term[{F}] := ⊢ N.app T →β* F All goals completed! 🐙example : Term[{N} {F}] →β* Term[{T}] := ⊢ N.app F →β* T All goals completed! 🐙
A Church numeral represents a natural number by iteration. Given a function
f and a starting value x, the numeral n applies f
exactly n times. Zero returns x immediately, while successor
adds one more application. Addition applies successor repeatedly.
def Z : Term := Term[λf. λx. x]def S : Term := Term[λn. λf. λx. f (n f x)]def Plus : Term := Term[λn. λm. n {S} m]def One : Term := Term[λf. λx. f x]def Two : Term := Term[λf. λx. f (f x)]example : Term[{One} f x] →β* Term[f x] := ⊢ (One.app (var "f")).app (var "x") →β* (var "f").app (var "x") All goals completed! 🐙example : Term[{Two} f x] →β* Term[f (f x)] := ⊢ (Two.app (var "f")).app (var "x") →β* (var "f").app ((var "f").app (var "x")) All goals completed! 🐙
The expressive freedom of untyped terms has a cost: reduction need not
terminate. Let SelfApply apply its argument to itself. Applying
SelfApply to itself produces exactly the same term again. The resulting
term Omega can therefore reduce forever and has no normal form.
This distinction is worth remembering: confluence will say that different
reduction choices are compatible, but it will not say that reduction
terminates. The simply typed calculus will later rule out Omega and
restore termination.
def Omega : Term := Term[(λx. x x) (λx. x x)]example : Omega →β Omega := ⊢ Omega →β Omega All goals completed! 🐙end Termend Untyped