SKI combinators
Three 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.
is not needed:
Bracket abstraction removes a variable from a combinator term, so that behaves like with for :
The last rule is correct because . A λ-term compiles innermost binder first.
A compiler from λ-terms to S, K, I, and a reducer for the result.
type lam = V of string | L of string * lam | A of lam * lamtype ski = S | K | I | Var of string | App of ski * skilet rec free x = function| Var y -> x = y| App (a, b) -> free x a || free x b| S | K | I -> false(* Bracket abstraction: [x] M is a combinator term that, applied to N,behaves like M with N for x. *)let rec abstract x = function| Var y when y = x -> I| m when not (free x m) -> App (K, m)| App (m, n) -> App (App (S, abstract x m), abstract x n)| _ -> assert falselet rec compile = function| V x -> Var x| A (m, n) -> App (compile m, compile n)| L (x, m) -> abstract x (compile m)(* Weak reduction: S x y z -> x z (y z), K x y -> x, I x -> x. *)let rec whnf t =match t with| App (I, x) -> whnf x| App (App (K, x), _) -> whnf x| App (App (App (S, x), y), z) -> whnf (App (App (x, z), App (y, z)))| App (f, x) ->let f' = whnf f inif f' == f then t else whnf (App (f', x))| _ -> tlet rec normal t = match whnf t with App (f, x) -> App (normal f, normal x) | t -> tlet rec show = function| S -> "S" | K -> "K" | I -> "I" | Var x -> x| App (f, (App _ as x)) -> show f ^ " (" ^ show x ^ ")"| App (f, x) -> show f ^ " " ^ show x
Running it.
S K K x -> xcompile (\x. \y. x) -> S (K K) Icompile flip -> S (S (K S) (S (K K) (S (K S) (S (S (K S) (S (K K) I)) (K I))))) (K (S (K K) I))(compile flip) g u v -> g v u
compiles to , which is only after the η-rule . flip, three binders deep, is already 25 combinators. Turner added and , for applications where only one side mentions the variable, and implemented SASL and Miranda this way:[3] compile to combinators, then reduce a graph of them.
see also
- 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.
- 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.
- 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.
- β-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.
- ApplicativeAn applicative functor is a functor with pure : a -> f a and an operation that combines independent computations, <*> : f (a -> b) -> f a -> f b, or equivalently a product f a -> f b -> f (a * b), satisfying four laws. It sits between functors and monads: every monad is applicative, but because later steps cannot depend on earlier results, an applicative computation can collect all errors, run its parts in parallel, or be inspected before it runs.
- 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.
further reading
- [1]M. Schönfinkel, “Über die Bausteine der mathematischen Logik”, Mathematische Annalen 92 (1924).
- [2]H. B. Curry, R. Feys, Combinatory Logic, Vol. I, North-Holland (1958).
- [3]D. A. Turner, “A new implementation technique for applicative languages”, Software: Practice and Experience 9 (1979).
last updated