Широко известно классическое индуктивное объявление натуральных чисел: ноль — натуральное число, и число следующее за натуральным — натуральное. На языке с поддержкой индуктивных типов это можно записать как-то так:
data Nat :: * where
Zero :: Nat
Succ :: Nat -> Nat
zero :: Nat
zero = Zero
one :: Nat
one = Succ Zero
two :: Nat
two = Succ (Succ Zero)
add :: Nat -> Nat -> Nat
add n Zero = n
add n (Succ k) = Succ (add n k)
mul :: Nat -> Nat -> Nat
mul n Zero = Zero
mul n (Succ k) = add m (mul n k)
pred :: Nat -> Nat
pred Zero = Zero
pred (Succ k) = k
minus :: Nat -> Nat -> Nat
minus Zero m = Zero
minus n Zero = n
minus (Succ k) (Succ l) = minus k l
Простейший способ кодирования таких чисел при помощи нетипизированного лямбда-исчисления — метод Чёрча, не имеющий, как кажется, прямого отношения к понятному индуктивному типу:
При этом используется нотация степени, являющаяся синтаксическим сахаром для следующей записи:
Несмотря на странный вид таких термов, некоторую аналогию найти можно: z в лямбда-термах будет означать ноль, а некоторое количество s перед ним — увеличение этого нуля на единицу несколько раз. Для получения еще более сильной аналогии введем функцию succ:
Действительно, воспользовавшись таким термом, мы можем получить ожидаемое поведение:
Также нам доступны простейшие арифметические операции (их можно проверить абсолютно аналогично тому, как выше проверяется функция succ):
Эти функции дают важное свойство натуральных чисел в кодировке Чёрча. Применение терма, являющегося n-ным натуральным числом к некоторым термам f и x обеспечит n-кратное применение f к x:
Вычитание и деление делаются несколько сложнее, с применением схемы примитивной рекурсии. Для этого обратимся к нескольким вспомогательным конструкциям. В первую очередь заведем пары из двух элементов. Индуктивное определение пары выглядит следующим образом:
data Pair :: * -> * -> * where
MkPair :: a -> b -> Pair a b
fst :: Pair a b -> a
fst (Pair x y) = x
snd :: Pair a b -> b
snd (Pair x y) = y
Мы же вновь должны пользоваться лямбда-исчислением для их кодирования:
Действительно, мы може записать любую пару элементов таким образом, ведь . Более того, нам доступны две проекции, позволяющие брать первый и второй элемент пары:
и здесь — это комбинаторы и соответственно. Действительно:
Как же воспользоваться этой конструкцией для получения предыдущего числа? Давайте зададим пару, которая будет содержать в качестве первого аргумента некоторое число, а в качестве второго — следующее за ним. Получить её довольно просто:
В частности, первые несколько таких пар будут (мы считаем, что предыдущее для ноля — ноль):
А можем ли мы вместо выписывания всех пар просто придумать терм, позволяющий нам получить следующую пару из предыдущей? Да, запросто!
Здесь первым элементом новой пары становится второй элемент предыдущей, а вторым — следующее число. Применив такую функцию к паре n раз мы получим пару, где второй элемент — число n, а первое — n - 1. А ровно это нам и требуется. Собирая все вместе получим функцию получения предыдущего натурального числа:
Из тех же соображений можно получить вычитание чисел:
Важным наблюдением тут является то, что функция pred работает за , а minus и вовсе за ! Это, очевидно, является проблемой кодировки натуральных чисел Чёрча.
Целочисленное деление также можно получить, вспомнив рекурсивное выражение через вычитание:
Для написания данного выражения нам потребуется еще один тип данных — Boolean, для того, чтоб выражать условие, в соответствии с которым мы будем возвращать 0 или идти в рекурсивную ветку. Классическое определение Boolean тривиально — это тип, который населяют два конктреных значения:
data Bool :: * where
True :: Bool
False :: Bool
ifThenElse :: Bool -> a -> a -> a
ifThenElse True x y = x
ifThenElse False x y = y
В нетипизированном лямбда-исчислении представление тоже не особо сложное:
Оператор if для такой кодировки совсем тривиальный:
Действительно, это можно проверить:
Для получения нашей формулы требуется еще понять, как сравнивать два числа. Простейший способ — вычесть одно из другого и сравнить с нулем. Вычитать мы уже умеем, а вот сравнение с нулем — это новое смешение двух миров: натуральных чисел и булевых значений. Тем не менее делается это сравнение очень просто. Заметим, что ноль выглядит так же, как false, а число отличное от нуля применит n раз какую-то функцию к нулю. Это позволяет нам написать функцию iszero по аналогии с if:
Теперь мы можем записать рекурсивную форму нашего деления:
Отлично! За одним исключением: мы не умеем работать с рекурсивными термами. Но это легко исправить. Для начала проабстрагируемся по упоминанию div в нашем терме:
Теперь наш терм принял вид , иными словами div является неподвижной точкой для терма:
В соответствии с теоремой о комбинаторе неподвижной точки такая точка есть у любого терма, и находится она просто с помощью применения такого комбинатора к терму:
Итого, финальное нерекурсивное выражение для деления:
Подведем итог. Мы увидели простые и очевидные определения для таких типов данных как натуральные числа, пары и булевы значения. Также мы определили набор функций для работы с каждым. Следующим шагом было появление из воздуха некоторых лямбда-термов, удивительным образом копирующих поведение этих конструкций. Применяя их заодно с построением примитивной рекурсии и выражением рекурсивых термов через Y-комбинатор, мы получили тот же набор функций. Вопрос же, на который мы постараемся ответить в следующий раз, а существует ли алгоритм, позволяющий переводить понятные индуктивные конструкции в лямбда-термы?