Кодирование Чёрча

В чистом λ-исчислении нет ни чисел, ни булевых значений, ни структур — только функции. Кодирование Чёрча показывает, что ничего другого и не нужно: данные — это их собственные операции обработки.

Начнём с булевых значений. И заметьте, как тут всё практично привязано. Не существует правды и лжи самих по себе: эти понятия имеют смысл только тогда, когда предполагают стратегии действия. В противном случае смысла в существовании правды и лжи нет. Прочувствуйте формулы.

tru=λtf.t,fls=λtf.f\mathbf{tru} = \lambda t\,f.\,t, \qquad \mathbf{fls} = \lambda t\,f.\,f
(1.11)

Здесь tt и ff — это просто два аргумента (мнемоника: ветка «true» и ветка «false»): tru\mathbf{tru} возвращает первый, а fls\mathbf{fls} — второй. То есть само булево значение — это уже выбор одного из двух, готовый if\mathbf{if}.

if=λbte.bte,not=λb.bflstru\mathbf{if} = \lambda b\,t\,e.\,b\,t\,e, \qquad \mathbf{not} = \lambda b.\,b\,\mathbf{fls}\,\mathbf{tru}
(1.12)

Здесь if\mathbf{if} почти ничего не делает сам: он берёт булево значение bb и отдаёт ему две ветки tt и ee, а bb уже выбирает нужную. not\mathbf{not} устроен так же просто: он даёт булевому значению ветки в обратном порядке, поэтому правда выбирает ложь, а ложь — правду.

and=λpq.pqp,or=λpq.ppq\mathbf{and} = \lambda p\,q.\,p\,q\,p, \qquad \mathbf{or} = \lambda p\,q.\,p\,p\,q
(1.13)

В and\mathbf{and} первое булево значение pp решает, что вернуть: если pp истинно, результат зависит от qq, а если ложно, сразу возвращается ложь. В or\mathbf{or} наоборот: если pp истинно, можно сразу вернуть правду, а если ложно, приходится смотреть на qq.

Подставим истину (1=tru1 = \mathbf{tru}, 0=fls0 = \mathbf{fls}) и произведём редукции:

not  tru  β  tru  fls  tru  β  fls(¬1=0)\mathbf{not}\;\mathbf{tru} \;\to_\beta\; \mathbf{tru}\;\mathbf{fls}\;\mathbf{tru} \;\twoheadrightarrow_\beta\; \mathbf{fls} \qquad (\lnot 1 = 0)
and  tru  tru  β  tru  tru  tru  β  tru(11=1)\mathbf{and}\;\mathbf{tru}\;\mathbf{tru} \;\twoheadrightarrow_\beta\; \mathbf{tru}\;\mathbf{tru}\;\mathbf{tru} \;\twoheadrightarrow_\beta\; \mathbf{tru} \qquad (1 \land 1 = 1)
or  tru  tru  β  tru  tru  tru  β  tru(11=1)\mathbf{or}\;\mathbf{tru}\;\mathbf{tru} \;\twoheadrightarrow_\beta\; \mathbf{tru}\;\mathbf{tru}\;\mathbf{tru} \;\twoheadrightarrow_\beta\; \mathbf{tru} \qquad (1 \lor 1 = 1)

Теперь поговорим о парах: это очень важный тип, который широко используется в Haskell. Пара — функция, хранящая два значения и ждущая «получателя»:

pair=λxyf.fxy,fst=λp.ptru,snd=λp.pfls\mathbf{pair} = \lambda x\,y\,f.\,f\,x\,y, \qquad \mathbf{fst} = \lambda p.\,p\,\mathbf{tru}, \qquad \mathbf{snd} = \lambda p.\,p\,\mathbf{fls}
(1.14)

fst\mathbf{fst} передаёт паре tru\mathbf{tru}, а snd\mathbf{snd}fls\mathbf{fls}, и та отдаёт нужную компоненту:

fst(pair  a  b)  β  tru  a  b  β  a\mathbf{fst}\,(\mathbf{pair}\;a\;b) \;\twoheadrightarrow_\beta\; \mathbf{tru}\;a\;b \;\twoheadrightarrow_\beta\; a
snd(pair  a  b)  β  fls  a  b  β  b\mathbf{snd}\,(\mathbf{pair}\;a\;b) \;\twoheadrightarrow_\beta\; \mathbf{fls}\;a\;b \;\twoheadrightarrow_\beta\; b

Заметьте, как интересно: функции fst\mathbf{fst} и snd\mathbf{snd} принимают аргумент-пару и передают ей управление — дальше она сама решает, что возвращать. Это, так сказать, функции-декораторы.

Теперь самое насущное — натуральные числа. Число nn — это «применить функцию nn раз»:

n=λfx.fn(x):0=λfx.x,1=λfx.fx,2=λfx.f(fx)\overline{n} = \lambda f\,x.\,f^{\,n}(x): \qquad \overline{0} = \lambda f\,x.\,x, \quad \overline{1} = \lambda f\,x.\,f\,x, \quad \overline{2} = \lambda f\,x.\,f\,(f\,x)
(1.15)

Важно: ff и xx здесь произвольные — их подставляет тот, кто пользуется числом. Нумерал фиксирует только «сколько раз применить», а не «что применять».

Скормим нумералу функцию ff и аргумент xx — и он применит ff ровно столько раз, сколько задаёт число:

0  f  x  β  x\overline{0}\;f\;x \;\twoheadrightarrow_\beta\; x
3  f  x  β  f(f(fx))\overline{3}\;f\;x \;\twoheadrightarrow_\beta\; f\,(f\,(f\,x))

Теперь научимся считать — вся арифметика вырастает из одного приёма «применить ff нужное число раз».

Начнём с прибавления единицы: succ\mathbf{succ} навешивает на число ещё одно применение ff:

succ=λnfx.f(nfx)\mathbf{succ} = \lambda n\,f\,x.\,f\,(n\,f\,x)
(1.16)
succ  n  β  λfx.f(nfx)  β  λfx.f(fn(x))  =  λfx.fn+1(x)  =  n+1\mathbf{succ}\;\overline{n} \;\to_\beta\; \lambda f\,x.\,f\,(\overline{n}\,f\,x) \;\twoheadrightarrow_\beta\; \lambda f\,x.\,f\bigl(f^{\,n}(x)\bigr) \;=\; \lambda f\,x.\,f^{\,n+1}(x) \;=\; \overline{n+1}

Сложение продолжает эту идею — применяет ff сначала n\overline{n} раз, а потом ещё m\overline{m}:

add=λmnfx.mf(nfx)\mathbf{add} = \lambda m\,n\,f\,x.\,m\,f\,(n\,f\,x)
(1.17)
add  m  n  β  λfx.mf(nfx)  β  λfx.fm(fn(x))  =  m+n\mathbf{add}\;\overline{m}\;\overline{n} \;\twoheadrightarrow_\beta\; \lambda f\,x.\,\overline{m}\,f\,(\overline{n}\,f\,x) \;\twoheadrightarrow_\beta\; \lambda f\,x.\,f^{\,m}\bigl(f^{\,n}(x)\bigr) \;=\; \overline{m+n}

Умножение — это уже повторное сложение:

mul=λmnf.m(nf)\mathbf{mul} = \lambda m\,n\,f.\,m\,(n\,f)
(1.18)
mul  m  n  β  λf.m(nf)  β  λfx.(fn)m(x)  =  λfx.fmn(x)  =  mn\mathbf{mul}\;\overline{m}\;\overline{n} \;\twoheadrightarrow_\beta\; \lambda f.\,\overline{m}\,(\overline{n}\,f) \;\twoheadrightarrow_\beta\; \lambda f\,x.\,\bigl(f^{\,n}\bigr)^{m}(x) \;=\; \lambda f\,x.\,f^{\,mn}(x) \;=\; \overline{mn}

Возведение в степень — повторное умножение, и в записи оно совсем короткое:

pow=λmn.nm\mathbf{pow} = \lambda m\,n.\,n\,m
(1.19)
pow  m  n  β  nm  β  mn\mathbf{pow}\;\overline{m}\;\overline{n} \;\to_\beta\; \overline{n}\,\overline{m} \;\twoheadrightarrow_\beta\; \overline{m^{\,n}}

В pow\mathbf{pow} аргументы просто меняются местами: nm\overline{n}\,\overline{m} — это m\overline{m}, применённое как функция nn раз, что и даёт mn\overline{m^{\,n}}.

Вычитание неожиданно трудно: pred\mathbf{pred} строится через пары — прогоняем по числу пару (i1,i)(i-1,\,i) и берём первую компоненту:

pred=λn.fst(n  (λp.pair(sndp)(succ(sndp)))  (pair00))\mathbf{pred} = \lambda n.\,\mathbf{fst}\,\bigl(n\;(\lambda p.\,\mathbf{pair}\,(\mathbf{snd}\,p)\,(\mathbf{succ}\,(\mathbf{snd}\,p)))\;(\mathbf{pair}\,\overline{0}\,\overline{0})\bigr)
(1.20)

По распространённой легенде Стивен Клини придумал функцию-предшественник (pred\mathbf{pred}, predecessor) — ключ к вычитанию нумералов Чёрча — прямо в кресле у стоматолога.

На pred\mathbf{pred} держится всё «вычитательное»: само вычитание — это многократный pred\mathbf{pred}, а деление — многократное вычитание. Отдельного λ-определения для деления не приводим: новой идеи там нет, тот же pred\mathbf{pred} в цикле, только запись громоздкая.

Проверим pred\mathbf{pred} на общем нумерале n\overline{n}. Обозначим шаг σ=λp.pair(sndp)(succ(sndp))\sigma = \lambda p.\,\mathbf{pair}\,(\mathbf{snd}\,p)\,(\mathbf{succ}\,(\mathbf{snd}\,p)) — он сдвигает пару: роняет первую компоненту и наращивает вторую:

σ(pair  a  b)  β  pair  b  (succ  b)\sigma\,(\mathbf{pair}\;a\;b) \;\twoheadrightarrow_\beta\; \mathbf{pair}\;b\;(\mathbf{succ}\;b)

Подставляем n\overline{n} в определение — это значит применить σ\sigma ровно nn раз к стартовой паре pair00\mathbf{pair}\,\overline{0}\,\overline{0}:

pred  n  β  fst(nσ(pair00))  β  fst(σn(pair00))\mathbf{pred}\;\overline{n} \;\to_\beta\; \mathbf{fst}\,\bigl(\overline{n}\,\sigma\,(\mathbf{pair}\,\overline{0}\,\overline{0})\bigr) \;\twoheadrightarrow_\beta\; \mathbf{fst}\,\bigl(\sigma^{\,n}(\mathbf{pair}\,\overline{0}\,\overline{0})\bigr)

Пары бегут по цепочке — вторая компонента считает шаги, первая отстаёт на единицу:

pair00    pair01    pair12        pairn1n\mathbf{pair}\,\overline{0}\,\overline{0} \;\to\; \mathbf{pair}\,\overline{0}\,\overline{1} \;\to\; \mathbf{pair}\,\overline{1}\,\overline{2} \;\to\; \cdots \;\to\; \mathbf{pair}\,\overline{n-1}\,\overline{n}

Осталось взять первую компоненту:

pred  n  β  fst(pairn1n)  β  n1\mathbf{pred}\;\overline{n} \;\twoheadrightarrow_\beta\; \mathbf{fst}\,(\mathbf{pair}\,\overline{n-1}\,\overline{n}) \;\twoheadrightarrow_\beta\; \overline{n-1}

У 0\overline{0} цепочка не двигается, поэтому pred0β0\mathbf{pred}\,\overline{0} \twoheadrightarrow_\beta \overline{0} — предшественником нуля считаем нуль.

iszero=λn.n(λz.fls)tru\mathbf{iszero} = \lambda n.\,n\,(\lambda z.\,\mathbf{fls})\,\mathbf{tru}
(1.21)

iszero\mathbf{iszero} скармливает числу функцию, которая на каждом шаге отвечает fls\mathbf{fls}, и старт tru\mathbf{tru}: у нуля шагов нет — остаётся tru\mathbf{tru}, а у любого другого числа хотя бы один шаг даёт fls\mathbf{fls}.

Список — его собственная правая свёртка (fold-кодирование):

[x1,,xn]    λcn.cx1(cx2((cxnn)))[x_1, \dots, x_n] \;\rightsquigarrow\; \lambda c\,n.\,c\,x_1\,(c\,x_2\,(\cdots(c\,x_n\,n)))
(1.22)

Тот же приём в Haskell — тип foldr\mathtt{foldr}: список полностью определяется тем, как он сворачивается. Нумерал Чёрча — «список из nn одинаковых применений».

-- список строится конструктором (:) (cons) и завершается [] (nil):
[1, 2, 3]  ==  1 : (2 : (3 : []))   -- True

-- Чёрч-кодирование — та же цепочка, только (:) и [] вынесены в параметры;
-- в Haskell это ровно foldr:
foldr (:) []  [1, 2, 3]   -- 1 : (2 : (3 : [])) = [1,2,3]   (собрали список обратно)
foldr (+) 0   [1, 2, 3]   -- 1 + (2 + (3 + 0))  = 6         (свернули в сумму)

Ну ведь есть же нормальное программирование, в котором есть и числа, и даже с плавающей точкой. Зачем всё это городить? Я хотел декларативного программирования, где всё просто, а тут... На самом деле ответ простой: ребята, если вы не любите цирк, вам там нечего делать, не приходите, но если вы любите цирк...