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

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

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

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

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

if\displaystyle \mathbf{if} =λb t e. b t e,\displaystyle {} = \lambda b\,t\,e.\,b\,t\,e,​not\displaystyle \mathbf{not}​=λb. b fls tru\displaystyle {} = \lambda b.\,b\,\mathbf{fls}\,\mathbf{tru}
(1.12)

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

and\displaystyle \mathbf{and} =λp q. p q p,\displaystyle {} = \lambda p\,q.\,p\,q\,p,​or\displaystyle \mathbf{or}​=λp q. p p q\displaystyle {} = \lambda p\,q.\,p\,p\,q
(1.13)

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

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

not  tru  \displaystyle \mathbf{not}\;\mathbf{tru} \; →β  tru  fls  tru  \displaystyle {} \to_\beta\; \mathbf{tru}\;\mathbf{fls}\;\mathbf{tru} \;​↠β  fls(¬1=0)\displaystyle {} \twoheadrightarrow_\beta\; \mathbf{fls} \qquad (\lnot 1 = 0)
and  tru  tru  \displaystyle \mathbf{and}\;\mathbf{tru}\;\mathbf{tru} \; ↠β  tru  tru  tru  \displaystyle {} \twoheadrightarrow_\beta\; \mathbf{tru}\;\mathbf{tru}\;\mathbf{tru} \;​↠β  tru(1∧1=1)\displaystyle {} \twoheadrightarrow_\beta\; \mathbf{tru} \qquad (1 \land 1 = 1)
or  tru  tru  \displaystyle \mathbf{or}\;\mathbf{tru}\;\mathbf{tru} \; ↠β  tru  tru  tru  \displaystyle {} \twoheadrightarrow_\beta\; \mathbf{tru}\;\mathbf{tru}\;\mathbf{tru} \;​↠β  tru(1∨1=1)\displaystyle {} \twoheadrightarrow_\beta\; \mathbf{tru} \qquad (1 \lor 1 = 1)

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

pair\displaystyle \mathbf{pair} =λx y f. f x y,\displaystyle {} = \lambda x\,y\,f.\,f\,x\,y,​fst\displaystyle \mathbf{fst}​=λp. p tru,\displaystyle {} = \lambda p.\,p\,\mathbf{tru},​snd\displaystyle \mathbf{snd}​=λp. p fls\displaystyle {} = \lambda p.\,p\,\mathbf{fls}
(1.14)

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

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

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

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

n‾\displaystyle \overline{n} =λf x. f n(x):0‾\displaystyle {} = \lambda f\,x.\,f^{\,n}(x): \qquad \overline{0}​=λf x. x,\displaystyle {} = \lambda f\,x.\,x,​1‾\displaystyle \overline{1}​=λf x. f x,\displaystyle {} = \lambda f\,x.\,f\,x,​2‾\displaystyle \overline{2}​=λf x. f (f x)\displaystyle {} = \lambda f\,x.\,f\,(f\,x)
(1.15)

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

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

0‾  f  x  \displaystyle \overline{0}\;f\;x \; ↠β  x\displaystyle {} \twoheadrightarrow_\beta\; x
3‾  f  x  \displaystyle \overline{3}\;f\;x \; ↠β  f (f (f x))\displaystyle {} \twoheadrightarrow_\beta\; f\,(f\,(f\,x))

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

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

succ\displaystyle \mathbf{succ} =λn f x. f (n f x)\displaystyle {} = \lambda n\,f\,x.\,f\,(n\,f\,x)
(1.16)
succ  n‾  \displaystyle \mathbf{succ}\;\overline{n} \; →β  λf x. f (n‾ f x)  \displaystyle {} \to_\beta\; \lambda f\,x.\,f\,(\overline{n}\,f\,x) \;​↠β  λf x. f(f n(x))  \displaystyle {} \twoheadrightarrow_\beta\; \lambda f\,x.\,f\bigl(f^{\,n}(x)\bigr) \;​=  λf x. f n+1(x)  \displaystyle {} =\; \lambda f\,x.\,f^{\,n+1}(x) \;​=  n+1‾\displaystyle {} =\; \overline{n+1}

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

add\displaystyle \mathbf{add} =λm n f x. m f (n f x)\displaystyle {} = \lambda m\,n\,f\,x.\,m\,f\,(n\,f\,x)
(1.17)
add  m‾  n‾  \displaystyle \mathbf{add}\;\overline{m}\;\overline{n} \; ↠β  λf x. m‾ f (n‾ f x)  \displaystyle {} \twoheadrightarrow_\beta\; \lambda f\,x.\,\overline{m}\,f\,(\overline{n}\,f\,x) \;​↠β  λf x. f m(f n(x))  \displaystyle {} \twoheadrightarrow_\beta\; \lambda f\,x.\,f^{\,m}\bigl(f^{\,n}(x)\bigr) \;​=  m+n‾\displaystyle {} =\; \overline{m+n}

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

mul\displaystyle \mathbf{mul} =λm n f. m (n f)\displaystyle {} = \lambda m\,n\,f.\,m\,(n\,f)
(1.18)
mul  m‾  n‾  \displaystyle \mathbf{mul}\;\overline{m}\;\overline{n} \; ↠β  λf. m‾ (n‾ f)  \displaystyle {} \twoheadrightarrow_\beta\; \lambda f.\,\overline{m}\,(\overline{n}\,f) \;​↠β  λf x. (f n)m(x)  \displaystyle {} \twoheadrightarrow_\beta\; \lambda f\,x.\,\bigl(f^{\,n}\bigr)^{m}(x) \;​=  λf x. f mn(x)  \displaystyle {} =\; \lambda f\,x.\,f^{\,mn}(x) \;​=  mn‾\displaystyle {} =\; \overline{mn}

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

pow\displaystyle \mathbf{pow} =λm n. n m\displaystyle {} = \lambda m\,n.\,n\,m
(1.19)
pow  m‾  n‾  \displaystyle \mathbf{pow}\;\overline{m}\;\overline{n} \; →β  n‾ m‾  \displaystyle {} \to_\beta\; \overline{n}\,\overline{m} \;​↠β  m n‾\displaystyle {} \twoheadrightarrow_\beta\; \overline{m^{\,n}}

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

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

pred\displaystyle \mathbf{pred} =λn. fst (n  (λp. pair (snd p) (succ (snd p)))  (pair 0‾ 0‾))\displaystyle {} = \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}. Обозначим шаг σ\sigma​=λp. pair (snd p) (succ (snd p)){} = \lambda p.\,\mathbf{pair}\,(\mathbf{snd}\,p)\,(\mathbf{succ}\,(\mathbf{snd}\,p)) — он сдвигает пару: роняет первую компоненту и наращивает вторую:

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

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

pred  n‾  \displaystyle \mathbf{pred}\;\overline{n} \; →β  fst (n‾ σ (pair 0‾ 0‾))  \displaystyle {} \to_\beta\; \mathbf{fst}\,\bigl(\overline{n}\,\sigma\,(\mathbf{pair}\,\overline{0}\,\overline{0})\bigr) \;​↠β  fst (σ n(pair 0‾ 0‾))\displaystyle {} \twoheadrightarrow_\beta\; \mathbf{fst}\,\bigl(\sigma^{\,n}(\mathbf{pair}\,\overline{0}\,\overline{0})\bigr)

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

pair 0‾ 0‾  \displaystyle \mathbf{pair}\,\overline{0}\,\overline{0} \; →  pair 0‾ 1‾  \displaystyle {} \to\; \mathbf{pair}\,\overline{0}\,\overline{1} \;​→  pair 1‾ 2‾  \displaystyle {} \to\; \mathbf{pair}\,\overline{1}\,\overline{2} \;​→  ⋯  \displaystyle {} \to\; \cdots \;​→  pair n−1‾ n‾\displaystyle {} \to\; \mathbf{pair}\,\overline{n-1}\,\overline{n}

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

pred  n‾  \displaystyle \mathbf{pred}\;\overline{n} \; ↠β  fst (pair n−1‾ n‾)  \displaystyle {} \twoheadrightarrow_\beta\; \mathbf{fst}\,(\mathbf{pair}\,\overline{n-1}\,\overline{n}) \;​↠β  n−1‾\displaystyle {} \twoheadrightarrow_\beta\; \overline{n-1}

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

iszero\displaystyle \mathbf{iszero} =λn. n (λz. fls) tru\displaystyle {} = \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]  \displaystyle [x_1, \dots, x_n] \; ⇝  λc n. c x1 (c x2 (⋯(c xn n)))\displaystyle {} \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         (свернули в сумму)

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