Higher-Order Abstract Syntax in Lean

2. Nameless, but Not Effortless🔗

De Bruijn indices turn α-equivalence into plain equality: a variable is the number of binders between it and its binder, so λx. x and λy. y are both lam (var 0).Named after Nicolaas de Bruijn, who introduced them in 1972 for the Automath proof checker. Lean's own terms are locally nameless: indices for bound variables, unique identifiers for free ones. Free variables are indices that point past the outermost binder: at the top level, var 0 is the first free variable. The chore moves into index arithmetic, since a term that moves under a binder must be shifted.

namespace DeBruijn inductive Term where | var : Nat → Term | lam : Term → Term | app : Term → Term → Term deriving Repr, DecidableEq open Term def Term.shift (c : Nat) : Term → Term | var k => var (if k < c then k else k + 1) | lam b => lam (b.shift (c + 1)) | app f a => app (f.shift c) (a.shift c) def Term.instantiate (s : Term) : Term → Term := go 0 s where go (j : Nat) (s : Term) : Term → Term | var k => if k < j then var k else if k = j then s else var (k - 1) | lam b => lam (go (j + 1) (s.shift 0) b) | app f a => app (go j s f) (go j s a) def Term.beta : Term → Term | app (lam b) a => b.instantiate a | t => t

beta performs the computation rule of the λ-calculus: applying λx. b to an argument a gives b[x := a]. It is called β-reduction. instantiate replaces index j by s, decrements the larger indices because one binder has disappeared, and shifts s each time it goes under a lam.

Take (λx. λy. x) y with y free. The result must be a constant function returning that free y, which now sits one binder deeper:

example : (app (lam (lam (var 1))) (var 0)).beta = lam (var 1) := ⊢ ((var 1).lam.lam.app (var 0)).beta = (var 1).lam All goals completed! 🐙 end DeBruijn

Forget one shift and the answer is lam (var 0), that is λy. y: capture again, just spelled in numbers.