Church encoding
Church encoding represents data in the pure λ-calculus, which has nothing but functions, by functions. The natural number is the function that applies its argument times, a boolean is a function that chooses between two alternatives, and a pair is a function waiting for a selector. In general a value is represented by its own fold: what it does to a given interpretation of its constructors. Arithmetic, logic and data structures all become λ-terms, which is how Church and Kleene showed that the λ-calculus can express every computable function[1][3]. In typed form the encoding needs polymorphism, and in System F it applies to every algebraic data type[6].
Overview
The λ-calculus has three kinds of term, variables, functions and applications , and one rule of computation, β-reduction, . It has no numbers, truth values or data structures. Church encoding supplies them by representing each kind of data by what can be done with it. A natural number is used for iteration, so the number is represented by the operation "iterate times"; a boolean is used to choose, so it is represented by a choice. Operations on data then become operations on these functions, and computation is β-reduction.
Church numerals
The Church numeral takes a function and a starting value and applies to times:
A numeral is converted to an ordinary number by applying it to the successor function and zero, and to anything else by choosing a different and .
Arithmetic
The arithmetic operations follow from the laws of iteration, and :
Exponentiation is application: composes with itself times, and the -fold composite of "iterate times" iterates times. The terms can be checked by reducing them to normal form:
An untyped λ-calculus with normal-order reduction, and Church arithmetic in it.
(* Untyped lambda terms with de Bruijn indices, and normal-orderreduction to normal form, counting beta-steps. *)type t = Var of int | Lam of t | App of t * tlet rec shift d c = function| Var k -> Var (if k >= c then k + d else k)| Lam b -> Lam (shift d (c + 1) b)| App (a, b) -> App (shift d c a, shift d c b)let rec subst j s = function| Var k -> if k = j then s else Var k| Lam b -> Lam (subst (j + 1) (shift 1 0 s) b)| 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)(* One leftmost-outermost step, if there is a redex. *)let rec step = function| App (Lam b, a) -> Some (beta b a)| App (a, b) -> (match step a with Some a' -> Some (App (a', b)) | None -> Option.map (fun b' -> App (a, b')) (step b))| Lam b -> Option.map (fun b' -> Lam b') (step b)| Var _ -> Nonelet normalize t =let rec go n t = match step t with Some t' -> go (n + 1) t' | None -> (t, n) ingo 0 t(* Terms are written with names, a % b for the application a b, andconverted to indices. *)type named = V of string | L of string * named | A of named * namedlet ( % ) a b = A (a, b)let rec index env = function| V x -> let rec find i = function y :: r -> if x = y then i else find (i + 1) r | [] -> failwith x in Var (find 0 env)| L (x, b) -> Lam (index (x :: env) b)| A (a, b) -> App (index env a, index env b)let lam xs b = List.fold_right (fun x b -> L (x, b)) xs b(* Church numerals and arithmetic. *)let church n = lam [ "f"; "x" ] (let rec go k = if k = 0 then V "x" else V "f" % go (k - 1) in go n)let succ = lam [ "n"; "f"; "x" ] (V "f" % (V "n" % V "f" % V "x"))let plus = lam [ "m"; "n"; "f"; "x" ] (V "m" % V "f" % (V "n" % V "f" % V "x"))let mult = lam [ "m"; "n"; "f" ] (V "m" % (V "n" % V "f"))let exp = lam [ "m"; "n" ] (V "n" % V "m")let pair = lam [ "a"; "b"; "s" ] (V "s" % V "a" % V "b")let fst_ = lam [ "p" ] (V "p" % lam [ "a"; "b" ] (V "a"))let snd_ = lam [ "p" ] (V "p" % lam [ "a"; "b" ] (V "b"))(* Kleene's predecessor: step (a, b) to (b, b + 1), n times from (0, 0). *)let pred_pairs =lam [ "n" ] (fst_ % (V "n" % lam [ "p" ] (pair % (snd_ % V "p") % (succ % (snd_ % V "p"))) % (pair % church 0 % church 0)))(* The usual predecessor, without pairs. *)let pred = lam [ "n"; "f"; "x" ] (V "n" % lam [ "g"; "h" ] (V "h" % (V "g" % V "f")) % lam [ "u" ] (V "x") % lam [ "u" ] (V "u"))(* Scott numerals: a number is its own case analysis. *)let scott n = let rec go k = if k = 0 then lam [ "z"; "s" ] (V "z") else lam [ "z"; "s" ] (V "s" % go (k - 1)) in go nlet scott_pred = lam [ "n" ] (V "n" % scott 0 % lam [ "m" ] (V "m"))(* Printing, with names chosen by binding depth. *)let names = [| "f"; "x"; "y"; "z"; "u"; "v"; "w" |]let rec show d = function| Var k -> names.(d - 1 - k)| Lam b -> "\\" ^ names.(d) ^ ". " ^ show (d + 1) b| App (a, b) -> show_fun d a ^ " " ^ show_arg d band show_fun d = function Lam _ as t -> "(" ^ show d t ^ ")" | t -> show d tand show_arg d = function Var _ as t -> show d t | t -> "(" ^ show d t ^ ")"let show = show 0
Normal forms and the number of β-steps to reach them.
succ 2 -> \f. \x. f (f (f x)) 3 stepsplus 2 3 -> \f. \x. f (f (f (f (f x)))) 6 stepsmult 2 3 -> \f. \x. f (f (f (f (f (f x))))) 7 stepsexp 2 3 -> \f. \x. f (f (f (f (f (f (f (f x))))))) 16 stepspred 3 -> \f. \x. f (f x) 11 steps
Predecessor
Subtracting one is much harder, because a numeral can only apply more times, never fewer. Kleene's solution iterates on pairs, carrying the previous value along:
Kleene later recalled finding it while at the dentist[4]. The predecessor usually given today avoids pairs, by iterating a function that delays each application of by one step and discarding the first:
Either way the predecessor of takes a number of β-steps proportional to , where a unary representation with a direct case analysis would take a constant number. The Scott encoding, which represents a number by its case analysis instead of its fold, has a constant-time predecessor:
β-steps to compute the predecessor of n, Church (with pairs and without) and Scott.
n pred (pairs) pred Scott pred1 16 7 42 26 9 44 46 13 48 86 21 416 166 37 432 326 69 464 646 133 4
Subtraction is iterated predecessor, , and takes time proportional to .
Booleans, pairs and sums
A boolean chooses between two alternatives, so returns the first and the second, and a conditional is the boolean itself applied to the two branches:
A pair holds two values until it is given a function to pass them to, and a sum, a value of one of two types, is given one function for each case:
With numerals, booleans and pairs, together with a fixed-point combinator for general recursion, every partial recursive function can be written as a λ-term. This was the content of Kleene's proof that λ-definability coincides with recursiveness[3], and with Turing's proof of its equivalence with Turing computability[14] it supports the Church–Turing thesis.
Lists and algebraic data types
A list is represented by its right fold: given a function to combine an element with the folded rest, and a value for the empty list, it returns the result of folding. Every algebraic data type can be encoded in the same way, by abstracting over one function per constructor, each receiving the constructor's arguments with recursive positions already folded. A natural number is the special case with constructors and , whose fold is iteration.
Church-encoded lists and binary trees in OCaml, where each value is its own fold.
(* A Church list is its own fold: what to do with each element and thefolded rest, and what to return for the empty list. *)type 'a clist = { fold : 'r. ('a -> 'r -> 'r) -> 'r -> 'r }let nil = { fold = (fun _ n -> n) }let cons x xs = { fold = (fun c n -> c x (xs.fold c n)) }let append xs ys = { fold = (fun c n -> xs.fold c (ys.fold c n)) }let map f xs = { fold = (fun c n -> xs.fold (fun x r -> c (f x) r) n) }let filter p xs = { fold = (fun c n -> xs.fold (fun x r -> if p x then c x r else r) n) }let length xs = xs.fold (fun _ r -> r + 1) 0let sum xs = xs.fold ( + ) 0let to_list xs = xs.fold List.cons []let of_list l = List.fold_right cons l nil(* A binary tree is its fold too. *)type 'a ctree = { foldt : 'r. 'r -> ('r -> 'a -> 'r -> 'r) -> 'r }let leaf = { foldt = (fun l _ -> l) }let node a x b = { foldt = (fun l n -> n (a.foldt l n) x (b.foldt l n)) }let size t = t.foldt 0 (fun a _ b -> a + 1 + b)let depth t = t.foldt 0 (fun a _ b -> 1 + max a b)let inorder t = to_list (t.foldt nil (fun a x b -> append a (cons x b)))
Running it.
map square [1; 4; 9; 16; 25]filter even [2; 4]append [1; 2; 3; 4; 5; 6; 7]length, sum 5, 15tree size 4, depth 3, in order [1; 2; 3; 4]
Operations that fold a structure are simple in this representation, and appending two lists is a composition of folds that does no work until the result is consumed. Operations that inspect a structure and stop are not: taking the tail of a Church list has the same difficulty as the predecessor, and takes time proportional to the length of the list[15].
Typing
Simple types
In the simply typed λ-calculus a numeral can be typed at for any fixed type , but each numeral then iterates over one type only. Schwichtenberg showed that the functions on definable in this way are exactly the extended polynomials, built from constants, addition, multiplication and a case distinction on zero[9]. Predecessor is not among them for a single type , nor is exponentiation, which uses a numeral at a different type from its argument.
System F
In System F, the polymorphic λ-calculus of Girard and Reynolds[7][8], the type of numerals quantifies over the type being iterated, and every algebraic data type has an encoding of this form[6]:
These are called Böhm–Berarducci encodings, after the authors who gave the general construction for any signature of constructors[6]. They are impredicative: quantifies over all types, including itself, which is what lets exponentiation and the pair-based predecessor type-check. Every closed normal term of type is, up to η-conversion, a Church numeral, so the encoding contains no junk; parametricity gives the same conclusion semantically and implies that the encoding satisfies the induction principle of the natural numbers[10].
In OCaml
ML type inference is limited to rank-1 polymorphism: a function's parameter has a single type within the function's body. A numeral passed as an argument therefore cannot be used at two different types:
A function that uses its numeral argument at int and at bool.
(* Without the record, a numeral passed as an argument has one type. *)let both n = (n (( + ) 1) 0, n not true)
The compiler's error.
File "ch2_bad.ml", line 2, characters 31-34:2 | let both n = (n (( + ) 1) 0, n not true)^^^Error: This expression has type bool -> boolbut an expression was expected of type int -> intType bool is not compatible with type int
A record or object with an explicitly polymorphic field restores the quantifier of the System F type, so the numeral can be instantiated at each use:
Church numerals and booleans with polymorphic record fields.
(* In OCaml a numeral must be polymorphic in the type it iterates over, soit is wrapped in a record with a polymorphic field. *)type nat = { run : 'a. ('a -> 'a) -> 'a -> 'a }let zero = { run = (fun _ x -> x) }let succ n = { run = (fun f x -> f (n.run f x)) }let plus m n = { run = (fun f x -> m.run f (n.run f x)) }let mult m n = { run = (fun f -> m.run (n.run f)) }let exp m n = { run = (fun f -> n.run m.run f) }let pred n = fst (n.run (fun (_, b) -> (b, succ b)) (zero, zero))let sub m n = n.run pred mlet to_int n = n.run (( + ) 1) 0let rec of_int k = if k = 0 then zero else succ (of_int (k - 1))(* Booleans choose between two alternatives. *)type bool_ = { choose : 'a. 'a -> 'a -> 'a }let tru = { choose = (fun t _ -> t) }let fls = { choose = (fun _ f -> f) }let not_ b = { choose = (fun t f -> b.choose f t) }let is_zero n = n.run (fun _ -> fls) trulet leq m n = is_zero (sub m n)(* The same numeral used at two different types. *)let both n = (n.run (( + ) 1) 0, n.run not true, n.run (fun s -> s ^ "*") "")
Running it.
plus 2 3 = 5 mult 2 3 = 6 exp 2 3 = 8 exp 3 2 = 9pred 3 = 2 pred 0 = 0 sub 3 2 = 1 sub 2 3 = 0leq 2 3 = true leq 3 2 = falseboth 3 = (3, false, "***")
Haskell writes the same types with RankNTypes, as newtype Nat = Nat (forall a. (a -> a) -> a -> a).
Uses
Church encodings appear in functional programming where representing data by its fold changes the cost of operations. The foldr/build fusion rule of the Glasgow Haskell Compiler removes intermediate lists: build takes a list in Church-encoded form, and a fold applied to it is replaced by an application, so the list is never constructed[11]. A free monad in Church-encoded form turns left-nested binds, which take quadratic time on the ordinary representation, into linear time[12]. The encoding of a data type as the type of its fold is also the basis of the Scott encoding, which represents a value by its case analysis instead.
In proof assistants based on impredicative type theories, Church encodings define data types in terms of quantification alone, though they do not give the dependent induction principles that inductive types provide, which is one reason those systems add inductive types as a primitive.
History
Church introduced the numerals in his work on the λ-calculus in the early 1930s, and used them in 1936 to state an undecidable problem[1]; the definitions appear in his 1941 monograph[2]. Kleene found the predecessor and went on to show that every recursive function is λ-definable[3]. Girard and Reynolds independently introduced the polymorphic λ-calculus in 1972 and 1974, in which the encodings have types[7][8], and Böhm and Berarducci gave the general encoding of algebraic data types in 1985[6]. The textbook treatment of the untyped encodings is in Barendregt[5] and, for programming, Pierce[13].
see also
- 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.
- 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.
- 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.
- Algebraic data typeAn algebraic data type is a type built from sums, where a value is one of several alternatives, and products, where a value has several components at once, possibly recursively. The name comes from counting: the number of values of a sum is the sum of the counts, and of a product their product, so types obey the laws of algebra. Values are taken apart by pattern matching, which the compiler checks for missing cases.
- HylomorphismA function that unfolds a seed into a recursive structure and folds that structure into a result, written so the structure is never built: hylo f g = f . fmap (hylo f g) . g. The call tree of the recursion is the intermediate structure.
- β-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.
- CurryingCurrying turns a function of several arguments into a chain of functions of one argument each: f : A * B -> C becomes curry f : A -> (B -> C). The two forms are in one-to-one correspondence, which in mathematics is the exponential law C^(A*B) = (C^B)^A. In OCaml and Haskell every function is curried, so partial application, supplying only some of the arguments, is ordinary.
- Free monadThe free monad on a functor F is the monad whose values are trees of F-shaped instructions with results at the leaves: Pure a | Free (F (Free F a)). It adds exactly the monad structure and nothing else, so a program written in it is a description of a sequence of instructions that different interpreters can run, inspect or transform. Any mapping from F into a monad M extends uniquely to an interpreter from the free monad into M. Left-nested binds take quadratic time, which the Church-encoded form avoids.
further reading
- [1]A. Church, “An unsolvable problem of elementary number theory”, American Journal of Mathematics 58 (1936).
- [2]A. Church, The Calculi of Lambda-Conversion, Annals of Mathematics Studies 6, Princeton University Press (1941).
- [3]S. C. Kleene, “λ-definability and recursiveness”, Duke Mathematical Journal 2 (1936).
- [4]J. N. Crossley, “Reminiscences of logicians”, Algebra and Logic, LNM 450 (1975).
- [5]H. P. Barendregt, The Lambda Calculus: Its Syntax and Semantics, North-Holland (revised ed., 1984).
- [6]C. Böhm, A. Berarducci, “Automatic synthesis of typed Λ-programs on term algebras”, Theoretical Computer Science 39 (1985).
- [7]J.-Y. Girard, Interprétation fonctionnelle et élimination des coupures de l’arithmétique d’ordre supérieur, thèse d’État, Université Paris 7 (1972).
- [8]J. C. Reynolds, “Towards a theory of type structure”, Programming Symposium, LNCS 19 (1974).
- [9]H. Schwichtenberg, “Definierbare Funktionen im λ-Kalkül mit Typen”, Archiv für mathematische Logik und Grundlagenforschung 17 (1976).
- [10]P. Wadler, “Theorems for free!”, Functional Programming Languages and Computer Architecture (1989).
- [11]A. Gill, J. Launchbury, S. L. Peyton Jones, “A short cut to deforestation”, Functional Programming Languages and Computer Architecture (1993).
- [12]J. Voigtländer, “Asymptotic improvement of computations over free monads”, Mathematics of Program Construction, LNCS 5133 (2008).
- [13]B. C. Pierce, Types and Programming Languages, ch. 5 and 23, MIT Press (2002).
- [14]A. M. Turing, “Computability and λ-definability”, Journal of Symbolic Logic 2 (1937).
- [15]J. M. Jansen, “Programming in the λ-calculus: from Church to Scott and back”, The Beauty of Functional Code, LNCS 8106 (2013).
last updated