SKI combinators
AdvancedThe SKI calculus is a tiny programming language with three built-in symbols — S, K, and I — and a single operation: function application. It is equivalent in power to the untyped lambda calculus, but has no variables. Anything you can compute with lambdas can also be expressed by combining S, K, and I.
The three rules are:
I xreduces tox.K x yreduces tox(drop the second argument).S f g xreduces to(f x) (g x)(apply bothfandgtox, then apply the first result to the second).
That is the entire semantics. There is no notion of binding, scope, or substitution beyond textual rewriting.
Minimal example
The identity combinator does what it says — given anything, return it unchanged. Press step to apply rules one at a time:
ski reduction
- 0I x
next rule: I x → x
K selects the first argument and discards the rest. Watch the second
argument disappear:
ski reduction
- 0K a b
next rule: K x y → x
A less obvious reduction
A classic identity in SKI: S K K x reduces to x. In other words,
S K K is the identity combinator, expressed without using I.
ski reduction
- 0S K K x
next rule: S f g x → (f x) (g x)
The first step rewrites the root with the S rule. The next step applies K
to the leftmost subterm. Walk through it carefully — every step is purely
mechanical, no insight required.
Practical example
Compose two functions, written here as opaque variables f and g. The SKI
encoding of "compose" is S (K S) K. Applying it to f, g, and an argument
x should reduce to f (g x):
ski reduction
- 0S (K S) K f g x
next rule: S f g x → (f x) (g x)
Press run to normal form. The result f (g x) is exactly function
composition — no lambdas, no environment, no names.
Why this matters
- The SKI calculus shows that variables are not fundamental — they are convenient sugar over a smaller core.
- Combinatory logic is the bridge from lambda calculus to point-free style
in real languages (Haskell's
(.)andflip, Clojure'scompandjuxt). - It is also the simplest setting in which to see what "reduction" really means: rewrite a redex by the rule for its head, repeat until no rules apply.
Try it in Clojure — SKI as combinators
Encode S, K, I as plain Clojure functions and watch the reductions happen by themselves at runtime.
comp, partial, juxt, and complement in clojure.core are
spiritually combinators in this exact sense — short, named, variable-free
function-builders. SKI is the theoretical floor of that style.
Real-world: where combinator thinking ships
| In production | The connection |
|---|---|
Haskell's Data.Function, Control.Arrow | ., flip, &, >>> are combinators by name |
| Unison language | Code is content-addressed trees of combinators — variables are sugar over this layer |
| Lambda lifting in compilers (GHC, MLton) | Compilers eliminate free variables by translating to a SKI-like combinator basis before code gen |
| Graph reduction machines (SPJ's G-machine, STG) | Lazy languages execute by repeated reduction of a combinator graph at runtime |
| Forth, concatenative languages (Joy, Factor) | The whole language is point-free — every program is a combinator pipeline |
| Ring middleware in Clojure, transducers | A middleware/transducer is a f → f combinator; full apps are built by composing them |
The takeaway: every time you write (comp f g) instead of (fn [x] (f (g x))),
you're moving one step closer to the SKI worldview — code as a tree of
operators rather than a tree of named bindings.
Exercise
Define T = S (K (S I)) K. Try reducing T a b and observe what it computes.
Hint: it is also a well-known combinator.
ski reduction
- 0S (K (S I)) K a b
next rule: S f g x → (f x) (g x)