Y combinator
The Y combinator is a λ-term such that for every : it finds a fixed point of any function, and so gives recursion to a language that has no named definitions. Under call-by-value evaluation it loops before is ever called; the Z combinator delays the self-application behind a λ and works in strict languages. No fixed-point combinator has a type in the simply typed λ-calculus, which is why typed languages provide recursion as a primitive instead.
Overview
A recursive definition such as refers to its own name. The pure λ-calculus has no names for definitions, only anonymous functions, so the recursion has to be expressed differently. First abstract the recursive call into a parameter:
Factorial is then a function with : a fixed point of . A fixed-point combinator produces one for any , so .
Definition
Two β-steps show that is a fixed point of :
The equation holds up to β-equality, not by reduction of to : both reduce to the same term. The self-application is what makes it work, since it lets a function receive itself as an argument. The fixed-point theorem of the untyped λ-calculus follows: every term has a fixed point, namely [3].
Other fixed-point combinators
is one of infinitely many. Turing's has the stronger property that reduces to , not just equals it[2]:
Evaluation order
Under call-by-name, arguments are passed unevaluated, and unfolds one step at a time as the body of demands its recursive call. Under call-by-value, arguments are evaluated before the call. In the argument is again, so evaluation unfolds forever, without ever entering . The Z combinator η-expands the self-application[4]:
is a λ, so it is a value, and it is passed to unevaluated. The self-application happens only when calls it with an argument, which is exactly when a recursive call is needed.
Y, Z and Turing's combinator in OCaml. Self-application needs a recursive datatype to type-check.
(* x x has no simple type; a recursive datatype gives it one. *)type 'a fix = Fix of ('a fix -> 'a)(* Y: loops under call-by-value, before f is ever entered. *)let y f = let g (Fix x as fx) = f (x fx) in g (Fix g)(* Z: the self-application waits behind a lambda for an argument. *)let z f = let g (Fix x as fx) = f (fun v -> x fx v) in g (Fix g)(* Turing's combinator, strict version: Theta = A A withA = fun x f -> f (fun v -> x x f v). *)let theta f =let a (Fix x as fx) = fun f -> f (fun v -> x fx f v) ina (Fix a) flet fact_step self n = if n = 0 then 1 else n * self (n - 1)
Running it.
z fact_step 10 -> 3628800theta fact_step 10 -> 3628800y fact_step 10 -> exception Stack_overflow
Typing
In the simply typed λ-calculus every term normalizes, so no term loops, and no fixed-point combinator can be typed. The obstruction is : would need a type equal to , which no finite simple type is. System F, with polymorphism, still normalizes and still has none[8].
Two things restore them. Recursive types allow directly, either through a datatype, as in the fix type above, or by switching on equi-recursive types, which OCaml does with -rectypes:
The same Z without the wrapper type, and the type of self-application.
(* With -rectypes, self-application type-checks directly. *)let z f = (fun x -> f (fun v -> x x v)) (fun x -> f (fun v -> x x v))let omega_type_of x = x x
What ocamlc -rectypes -i infers.
val z : (('a -> 'b) -> 'a -> 'b) -> 'a -> 'bval omega_type_of : ('a -> 'b as 'a) -> 'b
The type of omega_type_of is the cyclic type 'a -> 'b as 'a: a function that can be applied to itself. The other way is to add recursion as a primitive, which is what every typed functional language does: let rec in ML and OCaml, and in Haskell a library function fix f = let x = f x in x that relies on lazy, recursive let.
Semantics
In domain theory, a program denotes a continuous function on a partially ordered set of approximations, with a least element for "no information yet". Kleene's fixed-point theorem gives every continuous a least fixed point, the limit of its iterates from [7]:
For factorial, is the function that is correct on and undefined elsewhere, and the limit is correct everywhere. A recursive definition means this least fixed point, and computes it.
Open recursion
Writing a recursive function as a step function that takes its recursive call as a parameter has uses beyond encoding recursion. A different fixed-point operator can insert behaviour at every recursive call without changing the step function:
One Fibonacci step function run by a plain, a memoizing and a tracing fixed point.
(* Open recursion: the recursive call goes through a parameter, so adifferent fixed-point operator can insert behaviour at every call. *)let fib_step self n =if n < 2 then nelselet a = self (n - 1) inlet b = self (n - 2) ina + blet calls = ref 0(* The plain fixed point, counting calls. *)let rec fix f n = incr calls; f (fix f) n(* A fixed point that memoizes every call. *)let memo_fix f =let table = Hashtbl.create 64 inlet rec self n =match Hashtbl.find_opt table n with| Some v -> v| None -> incr calls; let v = f self n in Hashtbl.add table n v; vinself(* A fixed point that traces calls to a given depth. *)let trace_fix f =let depth = ref 0 inlet rec self n =if !depth < 3 then Printf.printf "%sfib %d\n" (String.make (2 * !depth) ' ') n;incr depth;let v = f self n indecr depth; vinself
Running it.
fix fib_step 25 -> 75025 (242785 calls)memo_fix fib_step 25 -> 75025 (26 calls)trace_fix fib_step 4, first three levels:fib 4fib 3fib 2fib 1fib 2fib 1fib 0
The same fib_step makes 242,785 calls under the plain fixed point and 26 under the memoizing one, which is dynamic programming obtained without editing the function. Object-oriented languages use the same idea for inheritance: a method calls other methods through self, and a subclass can override them.
Logic
Read as logic, a fixed point of implication is a contradiction. If a proposition could be defined as for arbitrary , then assuming gives and so ; hence holds, which is , and follows unconditionally. This is Curry's paradox[5], and it is the reason logics based on the untyped λ-calculus turned out inconsistent[6]. Type systems that correspond to consistent logics exclude general fixed points, and proof assistants that allow recursion check that it terminates.
History
Kleene and Rosser showed in 1935 that Church's original logic based on the λ-calculus was inconsistent[6], and Curry gave a simpler paradox of the same kind in 1942[5]. Turing published in 1937[2], and is usually attributed to Curry, who used it throughout combinatory logic[1]. Plotkin's 1975 study of call-by-value made the need for the η-expanded form precise[4]. The startup accelerator Y Combinator, founded in 2005, is named after the combinator.
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.
- Scott encodingRepresenting data by its case analysis rather than its fold: a Scott numeral takes what to do for zero and what to do with the predecessor. Pattern matching and the predecessor take one step; iteration needs recursion from outside, such as a fixed-point combinator.
- 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.
- ClosureA closure is a function value together with the environment in which it was created: the bindings of the free variables its body refers to. Closures are what make functions first-class values in a lexically scoped language. Compilers implement them by closure conversion, which turns each function into a code pointer paired with a record of captured values.
- β-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.
- 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.
further reading
- [1]H. B. Curry, R. Feys, Combinatory Logic, Vol. I, North-Holland (1958).
- [2]A. M. Turing, “The p-function in λ-K-conversion”, Journal of Symbolic Logic 2 (1937).
- [3]H. P. Barendregt, The Lambda Calculus: Its Syntax and Semantics, North-Holland (revised ed., 1984).
- [4]G. D. Plotkin, “Call-by-name, call-by-value and the λ-calculus”, Theoretical Computer Science 1 (1975).
- [5]H. B. Curry, “The inconsistency of certain formal logics”, Journal of Symbolic Logic 7 (1942).
- [6]S. C. Kleene, J. B. Rosser, “The inconsistency of certain formal logics”, Annals of Mathematics 36 (1935).
- [7]D. Scott, “Continuous lattices”, Toposes, Algebraic Geometry and Logic, LNM 274 (1972).
- [8]B. C. Pierce, Types and Programming Languages, ch. 5 and 12, MIT Press (2002).
last updated