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 encodingChurch encoding represents data in the pure lambda calculus, which has only functions, by functions: a natural number n is the function that applies its argument n times, a boolean is a function choosing between two alternatives, and a pair is a function waiting for a selector. In general a value is represented by its own fold. Arithmetic, logic and data structures all become lambda terms, which is how the lambda calculus was shown to express every computable function; in typed form the encoding needs polymorphism, as in System F.
- Y combinatorThe Y combinator is a lambda term Y such that Y f = f (Y f) for every f: it finds a fixed point of any function, and so gives recursion to a language with no named definitions. Under call-by-value it loops before f is ever called; the Z combinator delays the self-application behind a lambda and works in strict languages. No fixed-point combinator has a type in the simply typed lambda calculus.
- β-reductionβ-reduction is the computation rule of the lambda calculus: an application of a function to an argument, (λx. e) a, is replaced by the body e with a substituted for x. Substitution must avoid capturing free variables of the argument. A term with no reducible subterm is in normal form; by the Church–Rosser theorem the normal form, if it exists, is unique, and normal-order reduction, which always contracts the leftmost outermost redex, finds it. Evaluation strategies of programming languages are restrictions of β-reduction.
- Church–Rosser theoremThe Church–Rosser theorem states that β-reduction in the λ-calculus is confluent: if a term reduces to two different terms, both can be reduced further to a common term. It follows that a term has at most one normal form, so the order in which redexes are reduced cannot change the result of a terminating computation, and that the λ-calculus is consistent, since two different normal forms are never equal. Church and Rosser proved it in 1936. The standard modern proof, due to Tait and Martin-Löf, uses parallel reduction, which has the diamond property that single-step reduction lacks. Confluence is the central property of rewriting systems in general.
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).
last updated