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 has a fixed point — a term for which
Such a point is not hard to build explicitly. Take ; its single redex reduces in one β-step:
So , and in particular .
The term can be written as . 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:
Substituting into the theorem, we get the fixed-point identity — the working form of recursion:
Identity (1.25) is precisely an equality, not a reduction: no chain of direct β-steps takes to . Indeed, the single redex gives — this is the familiar from the proof of the theorem. From there the chain runs through , and so on, and the term never appears in it: inside the combinator no longer occurs. Meanwhile itself reduces to in a single step:
The two sides of the identity are equal only because they converge to the common reduct ; to obtain exactly from 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 (the future recursive call):
The fixed point of this template is exactly the factorial. With Curry's combinator the unfolding goes through β-equality:
This scheme is native to lazy languages (call-by-name/call-by-need). In Haskell the fixed-point combinator is built in — from — 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 does not typecheck in Hindley–Milner, because it sends type inference into a loop. So the fixed point is defined with a recursive (as in 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 would loop forever — to obtain the runtime immediately evaluates , for which it again needs , and so on without end, never reaching any useful work. Haskell is saved by laziness: is unfolded only when its value is actually demanded, so the same definition works without looping.