De Bruijn indices
A representation of lambda terms without variable names: each variable is the number of binders between it and the lambda that binds it. Alpha-equivalent terms become identical and substitution cannot capture a variable, at the price of shifting indices when a term moves under a binder.
The same variable can have different indices at different places, as does in the second term. With names, substitution has to avoid capture:
The free must not be captured by the binder, so named implementations rename bound variables first. With indices the question does not arise, because a free variable is a number larger than the binders around it, and substitution adjusts it when it goes under one. Shifting adds to the free variables of , those at or above the cutoff :
Substitution moves up a level under each binder, and β-reduction removes the binder and shifts back down:
Shift, substitution, and normal-order normalization.
(* Variables are numbers: the count of binders between the use and its lambda. *)type term = Var of int | Lam of term | App of term * term(* shift d c t: add d to every variable of t that is free, i.e. >= c. *)let rec shift d c = function| Var k -> Var (if k >= c then k + d else k)| Lam t -> Lam (shift d (c + 1) t)| App (a, b) -> App (shift d c a, shift d c b)(* subst j s t: replace variable j in t by s. Under a binder, both move up. *)let rec subst j s = function| Var k -> if k = j then s else Var k| Lam t -> Lam (subst (j + 1) (shift 1 0 s) t)| App (a, b) -> App (subst j s a, subst j s b)let beta body arg = shift (-1) 0 (subst 0 (shift 1 0 arg) body)let rec normal = function| App (f, a) -> (match normal f with| Lam body -> normal (beta body a)| f' -> App (f', normal a))| Lam t -> Lam (normal t)| Var k -> Var klet rec show = function| Var k -> string_of_int k| Lam t -> "\\." ^ show t| App (f, (App _ as a)) -> show f ^ " (" ^ show a ^ ")"| App ((Lam _ as f), a) -> "(" ^ show f ^ ") " ^ show a| App (f, a) -> show f ^ " " ^ show a
Running it.
K = \.\.1K applied to free 0 -> \.1plus 2 2 -> \.\.1 (1 (1 (1 0)))
applied to the free variable 0 gives : under the new binder, the same free variable is one further away. normalizes to the Church numeral 4.
α-equivalence becomes syntactic equality, which is why type checkers and proof assistants use indices internally, or levels counted from the outside in, and names only for display. The locally nameless style uses indices for bound variables and names for free ones, so terms compare structurally and free variables stay readable.
see also
- SKI combinatorsThree combinators, S x y z = x z (y z), K x y = x and I x = x, from which every closed lambda term can be built by application alone, with no variables. I is redundant, since S K K x = x. The translation from lambda terms is called bracket abstraction.
- Church encodingRepresenting data as functions in the pure lambda calculus: the numeral n applies its argument n times, true and false choose between two arguments, and a pair waits for a selector. Addition, multiplication and exponentiation are one line each; the predecessor needs a trick and takes time linear in n.
- Y combinatorA lambda term Y with Y f = f (Y f) for every f, which gives recursion to a language with no named definitions. Under call-by-value it loops before f is ever called; the Z combinator puts the self-application behind a lambda and works in strict languages.
further reading
- [1]N. G. de Bruijn, “Lambda calculus notation with nameless dummies, a tool for automatic formula manipulation, with application to the Church–Rosser theorem”, Indagationes Mathematicae 34 (1972).
- [2]B. C. Pierce, Types and Programming Languages, ch. 6, MIT Press (2002).
- [3]A. Charguéraud, “The locally nameless representation”, Journal of Automated Reasoning 49 (2012).