Curry's Y combinator

We have already built quite a lot on top of λ-terms — data and branching — yet loops are clearly missing. And in their classical form they will not appear at all: instead we get recursion. That is, we need to cook up a term that would spin any function forever. Finiteness we secure through branching, but that comes later.

The Earth's axis stays fixed while the planet itself rotates around it. The equation of the axis is the key characteristic of the rotation, and it is worth being able to derive it. With terms the analogy is the same, only we are after not an axis but a point that is fixed relative to a term. There is a theorem (the fixed-point theorem); I will state it loosely: every λ-term FF has a fixed point — a term XX for which

FX\displaystyle F\,X =βX.\displaystyle =_\beta X.
(1.23)

Such a point is not hard to build explicitly. Take XX (λx.F(xx))(λx.F(xx))\equiv (\lambda x.\,F\,(x\,x))\,(\lambda x.\,F\,(x\,x)); its single redex reduces in one β-step:

X\displaystyle X =(λx.F(xx))(λx.F(xx))  \displaystyle = (\lambda x.\,F\,(x\,x))\,(\lambda x.\,F\,(x\,x)) \; β  F((λx.F(xx))(λx.F(xx)))\displaystyle \to_\beta\; F\bigl((\lambda x.\,F\,(x\,x))\,(\lambda x.\,F\,(x\,x))\bigr) =FX.\displaystyle = F\,X.

So XX βFX\to_\beta F\,X, and in particular FXF\,X =βX=_\beta X.

The term XX can be written as (λf.(λx.f(xx))(λx.f(xx)))F(\lambda f.\,(\lambda x.\,f\,(x\,x))\,(\lambda x.\,f\,(x\,x)))\,F. We need to tear the fixed point out of the equation and spin the function directly with Curry’s combinator; the combinator is written as:

Y\displaystyle \mathrm{Y} =λf.(λx.f(xx))(λx.f(xx))\displaystyle = \lambda f.\,(\lambda x.\,f\,(x\,x))\,(\lambda x.\,f\,(x\,x))
(1.24)

Substituting XX =YF= \mathrm{Y}\,F into the theorem, we get the fixed-point identity — the working form of recursion:

YF  \displaystyle \mathrm{Y}\,F \; =β  F(YF)\displaystyle =_\beta\; F\,(\mathrm{Y}\,F)
(1.25)

Identity (1.25) is precisely an equality, not a reduction: no chain of direct β-steps takes YF\mathrm{Y}\,F to F(YF)F\,(\mathrm{Y}\,F). Indeed, the single redex gives YF\mathrm{Y}\,F βA\to_\beta A (λx.F(xx))(λx.F(xx))\equiv (\lambda x.\,F\,(x\,x))\,(\lambda x.\,F\,(x\,x)) — this is the familiar XX from the proof of the theorem. From there the chain runs through F(A)F\,(A), F(F(A))F\,(F\,(A)) and so on, and the term F(YF)F\,(\mathrm{Y}\,F) never appears in it: inside AA the combinator Y\mathrm{Y} no longer occurs. Meanwhile F(YF)F\,(\mathrm{Y}\,F) itself reduces to F(A)F\,(A) in a single step:

YF  \displaystyle \mathrm{Y}\,F \; β  A  \displaystyle \to_\beta\; A \; β  F(A),F(YF)  \displaystyle \to_\beta\; F\,(A), \qquad F\,(\mathrm{Y}\,F) \; β  F(A).\displaystyle \to_\beta\; F\,(A).

The two sides of the identity are equal only because they converge to the common reduct F(A)F\,(A); to obtain exactly F(YF)F\,(\mathrm{Y}\,F) from YF\mathrm{Y}\,F one would have to step against the arrow (a conversion).

Let us try this on the classic example — the factorial. The recursive step is given by a template with an extra parameter ff (the future recursive call):

F\displaystyle F =λfn.if(iszeron)  1  (muln(f(predn)))\displaystyle = \lambda f\,n.\,\mathbf{if}\,(\mathbf{iszero}\,n)\;\overline{1}\;(\mathbf{mul}\,n\,(f\,(\mathbf{pred}\,n)))

The fixed point of this template is exactly the factorial. With Curry's combinator the unfolding goes through β-equality:

fac  3\displaystyle \mathbf{fac}\;\overline{3} =YF  3\displaystyle = \mathrm{Y}\,F\;\overline{3} =βF(YF)  3\displaystyle =_\beta F\,(\mathrm{Y}\,F)\;\overline{3} =βif(iszero3)  1  (mul  3  (YF  2))\displaystyle =_\beta \mathbf{if}\,(\mathbf{iszero}\,\overline{3})\;\overline{1}\;(\mathbf{mul}\;\overline{3}\;(\mathrm{Y}\,F\;\overline{2})) =βmul  3  (YF  2)\displaystyle =_\beta \mathbf{mul}\;\overline{3}\;(\mathrm{Y}\,F\;\overline{2}) =βmul  3  (mul  2  (mul  1  (YF  0)))\displaystyle =_\beta \mathbf{mul}\;\overline{3}\;(\mathbf{mul}\;\overline{2}\;(\mathbf{mul}\;\overline{1}\;(\mathrm{Y}\,F\;\overline{0}))) =βmul  3  (mul  2  (mul  1  1))\displaystyle =_\beta \mathbf{mul}\;\overline{3}\;(\mathbf{mul}\;\overline{2}\;(\mathbf{mul}\;\overline{1}\;\overline{1})) =6\displaystyle = \overline{6}

This scheme is native to lazy languages (call-by-name/call-by-need). In Haskell the fixed-point combinator is built in — fix\mathtt{fix} from Data.Function\mathtt{Data.Function} — and it unfolds exactly as in (1.25), on demand:

fix :: (a -> a) -> a
fix f = let x = f x in x   -- the knot is tied by a lazy let: x refers to itself

fac :: Integer -> Integer
fac = fix (\f n -> if n == 0 then 1 else n * f (n - 1))

For all its usefulness in Haskell, Curry's combinator has two problems — and Haskell sidesteps both. The first is the type system: the self-application xxx\,x does not typecheck in Hindley–Milner, because it sends type inference into a loop. So the fixed point is defined with a recursive let\mathtt{let} (as in fix\mathtt{fix} above) or the self-application is hidden behind a newtype. What this system is and why it works this way is the subject of a separate section further on.

The second is the evaluation order: in a strict (call-by-value) language the definition let  x=f  x\mathtt{let\;x = f\;x} would loop forever — to obtain xx the runtime immediately evaluates f  xf\;x, for which it again needs xx, and so on without end, never reaching any useful work. Haskell is saved by laziness: xx is unfolded only when its value is actually demanded, so the same definition works without looping.