Комбинатор Карри

Мы уже многое построили на базе λ-термов, а именно: данные, ветвление, но вот явно не хватает циклов. Которых в своём классическом понимании и не будет вообще. Заместо этого будет рекурсия. То есть надо придумать такой терм, который бы крутил любую функцию бесконечно. Конечность мы обеспечим за счёт ветвления, но это будет потом.

Ось Земли является неподвижной, в то время как сама планета вращается вокруг этой оси. Уравнение оси — важнейшая характеристика вращения, и его хорошо бы уметь выводить. В случае с термами аналогия такая же, только мы ищем не ось, а точку, которая неподвижна относительно терма. Есть такая теорема (о неподвижной точке), сформулирую произвольно: у любого λ-терма FF есть неподвижная точка — терм XX, для которого

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

Такую точку нетрудно построить явно. Возьмём X(λx.F(xx))(λx.F(xx))X \equiv (\lambda x.\,F\,(x\,x))\,(\lambda x.\,F\,(x\,x)); единственный редекс сводится одной β-редукцией:

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

Итак, XβFXX \to_\beta F\,X, в частности FX=βXF\,X =_\beta X.

Терм XX можно представить как (λf.(λx.f(xx))(λx.f(xx)))F(\lambda f.\,(\lambda x.\,f\,(x\,x))\,(\lambda x.\,f\,(x\,x)))\,F. Нам надо вырвать неподвижную точку из уравнения, а функцию крутить непосредственно комбинатором Карри, который обозначается так:

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

Подставив X=YFX = \mathrm{Y}\,F в теорему, получаем тождество неподвижной точки — рабочую форму рекурсии:

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

Тождество (1.25) — именно равенство, а не редукция: никакая цепочка прямых β-шагов не приведёт YF\mathrm{Y}\,F к F(YF)F\,(\mathrm{Y}\,F). В самом деле, единственный редекс даёт YFβA(λx.F(xx))(λx.F(xx))\mathrm{Y}\,F \to_\beta A \equiv (\lambda x.\,F\,(x\,x))\,(\lambda x.\,F\,(x\,x)) — это знакомый нам XX из доказательства теоремы. Дальше цепочка идёт через F(A)F\,(A), F(F(A))F\,(F\,(A)) и так далее, и терма F(YF)F\,(\mathrm{Y}\,F) в ней нет: внутри AA комбинатор Y\mathrm{Y} уже не встречается. Зато F(YF)F\,(\mathrm{Y}\,F) сам за один шаг редуцируется в F(A)F\,(A):

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

Стороны тождества равны лишь потому, что сходятся к общему редукту F(A)F\,(A), а чтобы из YF\mathrm{Y}\,F получить именно F(YF)F\,(\mathrm{Y}\,F), пришлось бы шагнуть против стрелки (конверсия).

Опробуем это на классическом примере — факториале. Рекурсивный шаг задаём шаблоном с дополнительным параметром ff (будущим рекурсивным вызовом):

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

Неподвижная точка этого шаблона и есть факториал. С комбинатором Карри развёртка идёт через β-равенство:

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

Эта схема — родная для ленивых языков (call-by-name/call-by-need). В Haskell комбинатор неподвижной точки встроен — fix\mathtt{fix} из Data.Function\mathtt{Data.Function}, и развёртка идёт ровно как в (1.25), по мере надобности:

fix :: (a -> a) -> a
fix f = let x = f x in x   -- узел завязан ленивым let: x ссылается сам на себя

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

У комбинатора Карри, при всей его применимости в Haskell, есть две проблемы — и Haskell обе обходит. Первая — система типов: самоаппликация xxx\,x в системе Хиндли — Милнера не типизируется, потому что приводит к зацикливанию вывода типов. Поэтому неподвижную точку задают рекурсивным let\mathtt{let} (как в fix\mathtt{fix} выше) или прячут самоаппликацию за newtype. Что это за система и почему она так устроена — тема отдельного раздела дальше.

Вторая — энергичный порядок вычислений: в строгом (call-by-value) языке определение let  x=f  x\mathtt{let\;x = f\;x} крутилось бы вечно — чтобы получить xx, среда сразу вычисляет f  xf\;x, для этого снова xx, и так без конца, не добираясь до полезной работы. Haskell спасает ленью: xx раскрывается лишь тогда, когда его значение действительно понадобится, поэтому та же запись работает без зацикливания.