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 combinatorA lambda term Y with Y f = f (Y f) for every f, which gives recursion to a language with no named definitions. Under call-by-value it loops before f is ever called; the Z combinator puts the self-application behind a lambda and works in strict languages.
- Church encodingRepresenting data as functions in the pure lambda calculus: the numeral n applies its argument n times, true and false choose between two arguments, and a pair waits for a selector. Addition, multiplication and exponentiation are one line each; the predecessor needs a trick and takes time linear in n.
- 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.
- CurryingTurning a function of several arguments into a function of the first argument that returns a function of the rest: f : A * B -> C becomes curry f : A -> (B -> C). In OCaml and Haskell every function is curried, so applying it to fewer arguments than it takes, partial application, 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).