β-reduction
β-reduction is the rule of computation of the λ-calculus: an application of a function to an argument, , is replaced by the body with substituted for . Substitution has to rename bound variables where necessary so that no free variable of the argument is captured. A term that contains no application of this form is in normal form. By the Church–Rosser theorem, the order in which reductions are performed cannot lead to two different normal forms[1], and normal-order reduction, which always reduces the leftmost outermost application first, reaches the normal form whenever there is one[3]. The evaluation strategies of programming languages, call-by-value, call-by-name and call-by-need, are restrictions of β-reduction[4].
Definition
A β-redex is a term of the form , and its contractum is , the body with the argument substituted for the parameter:
The one-step reduction relation contracts one redex anywhere in a term, including under a λ and inside an argument. Its reflexive transitive closure is multi-step reduction, and the equivalence it generates, , is β-conversion. A term with no redex is in β-normal form. The λ-calculus has two further rules. α-conversion renames a bound variable, when is not free in , and terms are normally considered up to it. η-reduction removes a redundant abstraction, when is not free in [2].
Capture-avoiding substitution
Substitution replaces the free occurrences of a variable. It is defined by recursion on the term[2]:
The last case renames the bound variable before substituting under it. Without it, a free variable of the argument that happens to share its name with a binder in the body would become bound, and the meaning of the term would change:
Named λ-terms with a parser and printer, used by the programs below.
(* Named lambda terms, a small parser, and printing. *)type t = Var of string | Lam of string * t | App of t * tlet rec free = function| Var x -> [ x ]| Lam (x, b) -> List.filter (( <> ) x) (free b)| App (a, b) -> free a @ free b(* Parser for terms like \x y. x (y z). *)let parse s =let n = String.length s and i = ref 0 inlet rec ws () = if !i < n && s.[!i] = ' ' then (incr i; ws ()) inlet ident () =ws ();let j = !i inwhile !i < n && (match s.[!i] with 'a' .. 'z' | 'A' .. 'Z' | '0' .. '9' | '\'' | '_' -> true | _ -> false) do incr i done;String.sub s j (!i - j)inlet rec term () =ws ();if !i < n && s.[!i] = '\\' then (incr i;let rec params acc = ws (); if s.[!i] = '.' then (incr i; List.rev acc) else params (ident () :: acc) inlet xs = params [] inList.fold_right (fun x b -> Lam (x, b)) xs (term ()))elselet rec apps f = ws (); if !i < n && s.[!i] <> ')' then apps (App (f, atom ())) else f inapps (atom ())and atom () =ws ();if s.[!i] = '(' then (incr i; let t = term () in ws (); incr i; t)else if s.[!i] = '\\' then term ()else Var (ident ())interm ()let rec show = function| Var x -> x| Lam (x, b) -> "\\" ^ x ^ ". " ^ show b| App (a, b) ->(match a with Lam _ -> "(" ^ show a ^ ")" | _ -> show a) ^ " " ^ (match b with Var x -> x | _ -> "(" ^ show b ^ ")")let rec size = function Var _ -> 1 | Lam (_, b) -> 1 + size b | App (a, b) -> 1 + size a + size b
Naive substitution and capture-avoiding substitution.
(* Naive substitution: replaces free x in t by s, ignoring capture. *)let rec subst_naive x s = function| Var y -> if y = x then s else Var y| Lam (y, b) -> if y = x then Lam (y, b) else Lam (y, subst_naive x s b)| App (a, b) -> App (subst_naive x s a, subst_naive x s b)(* Capture-avoiding substitution: rename a binder that would capture afree variable of s. *)let rec fresh y avoid = if List.mem y avoid then fresh (y ^ "'") avoid else ylet rec subst x s = function| Var y -> if y = x then s else Var y| App (a, b) -> App (subst x s a, subst x s b)| Lam (y, b) when y = x -> Lam (y, b)| Lam (y, b) when List.mem y (free s) && List.mem x (free b) ->let y' = fresh y (free s @ free b) inLam (y', subst x s (subst y (Var y') b))| Lam (y, b) -> Lam (y, subst x s b)(* One beta-step at the root, if the term is a redex. *)let contract subst = function App (Lam (x, b), a) -> Some (subst x a b) | _ -> None
Contracting three redexes with each.
(\x. \y. x) ynaive: \y. ycapture-avoiding: \y'. y(\x. \y. x y) (\z. y)naive: \y. (\z. y) ycapture-avoiding: \y'. (\z. y) y'(\f. \x. f (f x)) xnaive: \x. x (x x)capture-avoiding: \x'. x (x x')
In the first example, should be a constant function returning the free , and naive substitution produces the identity function instead. Implementations avoid the problem by generating fresh names, or by eliminating names altogether with de Bruijn indices, in which a variable is the number of binders between it and its own binder, so that no renaming is ever needed[10].
Confluence
A term can contain several redexes, and reducing different ones leads to different terms. The Church–Rosser theorem says that the choice does not matter in the end[1]:
Two consequences follow. A term has at most one normal form, up to α-conversion, so the normal form can be regarded as the value of the term. And two terms are β-convertible exactly when they reduce to a common term, so the λ-calculus is consistent: distinct normal forms such as and are not equal. The standard modern proof uses parallel reduction, which contracts any set of redexes present in a term simultaneously and has the diamond property[11].
Normal forms and strategies
Not every term has a normal form. reduces only to itself. Some terms have a normal form that one order of reduction finds and another misses: reduces to in one step if the outer redex is contracted, and forever if is reduced first. A reduction strategy chooses which redex to contract:
Normal order contracts the leftmost outermost redex, so a function is applied before its argument is reduced. Applicative order contracts the leftmost innermost redex, so arguments are reduced to normal form before they are passed.
Normal-order and applicative-order reduction, with a step limit, and a trace.
let rec fresh y avoid = if List.mem y avoid then fresh (y ^ "'") avoid else ylet rec subst x s = function| Var y -> if y = x then s else Var y| App (a, b) -> App (subst x s a, subst x s b)| Lam (y, b) when y = x -> Lam (y, b)| Lam (y, b) when List.mem y (free s) && List.mem x (free b) ->let y' = fresh y (free s @ free b) in Lam (y', subst x s (subst y (Var y') b))| Lam (y, b) -> Lam (y, subst x s b)(* Normal order: contract the leftmost, outermost redex. *)let rec normal = function| App (Lam (x, b), a) -> Some (subst x a b)| App (a, b) -> (match normal a with Some a' -> Some (App (a', b)) | None -> Option.map (fun b' -> App (a, b')) (normal b))| Lam (x, b) -> Option.map (fun b' -> Lam (x, b')) (normal b)| Var _ -> None(* Applicative order: contract the leftmost, innermost redex, so anargument is normalized before it is passed. *)let rec applicative = function| App (a, b) -> (match applicative a with| Some a' -> Some (App (a', b))| None -> (match applicative b with| Some b' -> Some (App (a, b'))| None -> (match a with Lam (x, body) -> Some (subst x b body) | _ -> None)))| Lam (x, b) -> Option.map (fun b' -> Lam (x, b')) (applicative b)| Var _ -> Nonelet reduce ?(limit = 1000) step t =let rec go n t = if n = limit then None else match step t with Some t' -> go (n + 1) t' | None -> Some (t, n) ingo 0 tlet trace step t =let rec go t = print_endline (" " ^ show t); match step t with Some t' -> go t' | None -> () ingo t
Running it.
K z Omega, the argument is discarded: (\x. z) ((\w. w w) (\w. w w))normal z 1 stepsapplicative no normal form after 1000 stepsan argument used three times: (\x. x x x) ((\y. y) a)normal a a a 4 stepsapplicative a a a 2 stepsan argument never used: (\x. z) ((\y. y y y) ((\w. w) a))normal z 1 stepsapplicative z 3 stepsnormal-order reduction of (\x y. y x) a (\z. z z):(\x. \y. y x) a (\z. z z)(\y. y a) (\z. z z)(\z. z z) aa a
Normal order finds the normal form of and applicative order loops. The standardization theorem guarantees that this is general: if a term has a normal form, normal-order reduction reaches it[3][2]. Normal order is not always efficient, though. When the argument is used three times it is copied unreduced and then reduced three times, in four steps against two; when it is not used, normal order saves the work of reducing it. No strategy that reduces terms is optimal for every term. Lévy characterized optimal reduction, which shares the reduction of copied redexes[12], and Lamping gave an algorithm for it based on graph reduction[8].
Evaluation in programming languages
Programming languages evaluate programs, not arbitrary terms: they stop at a value and never reduce under a λ. Call-by-value, used by ML, OCaml and most languages, contracts a redex only when its argument is a value, which resembles applicative order without reduction under λ. Call-by-name contracts the outermost redex, like normal order. Plotkin gave the two their precise definitions and relationship, along with the CPS transforms that simulate each in the other[4]. Call-by-need, the basis of lazy languages such as Haskell, is call-by-name with sharing: an argument is evaluated at most once, the first time it is needed, and its value is shared by every copy, an idea Wadsworth introduced as graph reduction[7].
Cost
A β-step can copy its argument any number of times, so the size of a term can grow exponentially in the number of steps. Church numerals show it: the term has size linear in , and its normal form, the numeral for , has size exponential in :
Reducing n̄ 2̄ to normal form.
let rec fresh y avoid = if List.mem y avoid then fresh (y ^ "'") avoid else ylet rec subst x s = function| Var y -> if y = x then s else Var y| App (a, b) -> App (subst x s a, subst x s b)| Lam (y, b) when y = x -> Lam (y, b)| Lam (y, b) when List.mem y (free s) && List.mem x (free b) ->let y' = fresh y (free s @ free b) in Lam (y', subst x s (subst y (Var y') b))| Lam (y, b) -> Lam (y, subst x s b)let rec normal = function| App (Lam (x, b), a) -> Some (subst x a b)| App (a, b) -> (match normal a with Some a' -> Some (App (a', b)) | None -> Option.map (fun b' -> App (a, b')) (normal b))| Lam (x, b) -> Option.map (fun b' -> Lam (x, b')) (normal b)| Var _ -> Nonelet rec nf n t = match normal t with Some t' -> nf (n + 1) t' | None -> (t, n)(* The Church numeral n, as a string: \f x. f (f (... x)). *)let church n = "(\\f x. " ^ String.concat "" (List.init n (fun _ -> "f (")) ^ "x" ^ String.make n ')' ^ ")"
Running it.
n term size normal form steps1 13 7 22 15 11 64 19 35 306 23 131 1268 27 515 51010 31 2051 2046
Counting β-steps is therefore not obviously a reasonable measure of time. Accattoli and Dal Lago showed that it is, for normal-order reduction: the number of leftmost-outermost steps to normal form is polynomially related to the time a Turing machine needs to compute it, provided the normal form is represented with sharing[9].
Typed λ-calculi
In the simply typed λ-calculus every reduction sequence terminates, a property called strong normalization; Tait proved it with the method of reducibility candidates, and Girard extended the method to System F[5][6]. Under the Curry–Howard correspondence, a β-redex is a proof that introduces a connective and immediately eliminates it, and β-reduction is the removal of such detours, the normalization of proofs[6]. Subject reduction, the property that reduction preserves types, is the formal core of the statement that well-typed programs do not go wrong[13].
History
Church introduced the λ-calculus in the early 1930s, with the conversion rules that were later named α, β and η. Church and Rosser proved confluence in 1936[1], and Curry and Feys proved the standardization theorem, from which the normalizing property of normal order follows[3]. Wadsworth's 1971 thesis introduced graph reduction and call-by-need[7], de Bruijn his nameless notation in 1972[10], and Plotkin in 1975 related the calculus to the call-by-name and call-by-value evaluation of programming languages[4]. Barendregt's monograph of 1981, revised in 1984, is the standard reference[2].
see also
- De Bruijn indicesA 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.
- 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.
- 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.
- Continuation-passing styleContinuation-passing style (CPS) is a way of writing programs in which no function returns: each takes an extra argument, its continuation, a function that represents the rest of the computation, and calls it with the result. Every call becomes a tail call, evaluation order is made explicit, and control operators such as exceptions, backtracking and call/cc become ordinary functions. Compilers for functional languages use CPS, or the closely related A-normal form, as an intermediate language.
- Lazy evaluationLazy evaluation, or call-by-need, delays computing an expression until its value is needed and then remembers the value, so it is computed at most once. It lets programs define infinite structures and consume only part of them, separates generating candidate values from choosing among them, and avoids work whose result is never used. Its costs are the memory held by unevaluated suspensions, which can cause space leaks, and harder reasoning about when work happens. Haskell is lazy by default; OCaml provides it explicitly through Lazy.t and Seq.
further reading
- [1]A. Church, J. B. Rosser, “Some properties of conversion”, Transactions of the American Mathematical Society 39 (1936).
- [2]H. P. Barendregt, The Lambda Calculus: Its Syntax and Semantics, North-Holland (revised ed., 1984).
- [3]H. B. Curry, R. Feys, Combinatory Logic, Vol. I, North-Holland (1958).
- [4]G. D. Plotkin, “Call-by-name, call-by-value and the λ-calculus”, Theoretical Computer Science 1 (1975).
- [5]W. W. Tait, “Intensional interpretations of functionals of finite type I”, Journal of Symbolic Logic 32 (1967).
- [6]J.-Y. Girard, Y. Lafont, P. Taylor, Proofs and Types, Cambridge University Press (1989).
- [7]C. P. Wadsworth, Semantics and Pragmatics of the Lambda-Calculus, DPhil thesis, University of Oxford (1971).
- [8]J. Lamping, “An algorithm for optimal lambda calculus reduction”, POPL (1990).
- [9]B. Accattoli, U. Dal Lago, “(Leftmost-outermost) beta reduction is invariant, indeed”, Logical Methods in Computer Science 12 (2016).
- [10]N. G. de Bruijn, “Lambda calculus notation with nameless dummies”, Indagationes Mathematicae 34 (1972).
- [11]M. Takahashi, “Parallel reductions in λ-calculus”, Information and Computation 118 (1995).
- [12]J.-J. Lévy, Réductions correctes et optimales dans le lambda-calcul, thèse d’État, Université Paris 7 (1978).
- [13]B. C. Pierce, Types and Programming Languages, ch. 5, MIT Press (2002).
last updated