Лямбда-исчисление (Алонзо Чёрч, начало 1930х) – простая, но очень ёмкая математическая модель вычислительного устройства. Относится к классу т.н. “систем правил переписывания”, к которым также относится рассмотренная нами ранее нормализация высказываний.
Как и любая система правил переписывания, лямбда-исчисление постулирует некоторую структуру формул, а также правила (точнее, ровно одно правило) преобразования этих формул. Кроме того, поскольку лямбда-исчисление не гарантирует, что выражение когда-либо окажется в нормальной форме, существуют многочисленные способы задать порядок преобразований. При этом иногда оказывается интересен не сам результат преобразований, а промежуточные шаги.
Отметим напоследок, что в лямда-исчислении у каждого выражения существует не более одной нормальной формы. Это следует из известной теоремы Чёрча-Россера.
Требуется наличие одной бинарной операции (её будем обозначать *
или вообще опускать) и одной унарной связывающей операции λ
.
Под связывающей операцией понимается операция, в операндах которой в качестве “листьев” допускаются “привязки”. Обычно, для удобства отображения и восприятия, эти привязки обозначаются какой-нибудь буквой, хотя их не следует считать полноценными буквами. Например, формулу
λx λy x x y
следует воспринимать как
___________
| |
λ λ (_ * _) * _
|____|___|
Эта формула та же, что и, например,
λa λb a a b
Отметим, что подформула лямбды записывается между знаком “лямбда” и ближайшей закрывающей скобкой, не относящейся к этой подформуле: ровно того же соглашения придерживаются в математике при написании сумм, произведений и интегралов. Договоримся также в случаях наподобие λx λx x
расставлять “привязки” из глубины наружу, то есть так: λx λy y
.
В лямбда исчислении ровно одно правило преобразования: “подстановка”. Выражение вида (λx F) G
разрешается заменить на F[x/G]
. Последняя запись расшифровывается как: “заменить все привязки лямбды в подформуле F
на формулу G
”.
Ни в коем случае не следует воспринимать преобразование (λx F) G → F[x/G]
буквально: рассмотрим, например, формулу
(λx λy x y) y
в которой правая y
– просто буква (операция арности 0). Результатом преобразования этой формулы является
λz y z
а не λy y y
, как можно было бы подумать.
Есть два широких класса порядков вычислений (с большим количество разных вариаций): энергичный и ленивый. Основное различие – работа с выражением вида A*B
.
Энергичный порядок (или, как ещё говорят, аппликативный) регламентирует следующую процедуру преобразования A*B
:
B
, получив b
.A
, получив a
.a=(λx F)
, то перейти от A*B
к F[x/b]
и повторить все три пункта.Ленивый порядок отличается от аппликативного отсутствием пункта 1. При дальнейшем рассмотрении лямбда-исчисления мы будем использовать т.н. “нормализующий ленивый порядок”. Он очень прост: нужно на каждом шаге применять преобразование к самой левой лямбде среди тех, к которым можно его применить.
Есть важный (но нетривиально доказываемый) факт: “нормализующий ленивый порядок” приводит выражение к нормальной форме, если она есть.
В лямбда-исчислении редко рассматривают отношение равенства на множестве формул: оно оторвано от практики. Гораздо более полезны и часто встречаются отношения нормальной эквивалентности и экстенсиональной эквивалентности.
Формулы F
и G
называются нормально эквивалентными, если у них одна и та же нормальная форма (или же нормальной формы нет ни у F
, ни у G
). При рассмотрении ненормализующих порядков вычислений под нормальной эквивалентностью обычно понимают одинаковый результат преобразования F
и G
: либо оба приводятся к нормальной форме, либо оба зацикливаются.
Формулы F
и G
называются экстенсионально эквивалентными, если для любого x
выражения Fx
и Gx
нормально эквивалентны.
В каком-то смысле нормальная эквивалентность является экстенсиональной эквивалентностью “нулевого порядка”. Можно рассматривать и экстенсиональную эквивалентность второго порядка: F
и G
вторично эквивалентны, если для любого x
выражения Fx
и Gx
экстенсионально эквивалентны.
Иногда вводят специальное обозначение ⊥
для какого-нибудь зацикливающегося выражения. Тогда высказывание о зацикивании F
(логически) эквивалентно высказыванию о нормальной эквивалентности F
и ⊥
.
В завершение разговора об экстенсиональной эквивалентности приведём несколько примеров.
Выражения (λx x x) (λx x x)
и ⊥
нормально эквивалентны.
Выражения (λx (λy y) x)
и (λy y)
экстенсионально эквивалентны.
Более общо, выражения λx F x
и F
экстенсионально эквивалентны.
А вот выражения λx (λy F y)
и λx F
экстенсионально эквивалентными не являются (хотя являются вторично-экстенсионально эквивалентными).
Выражения λk k k ((λx x x) (λx x x))
и (λx x x) (λx x x)
нормально эквивалентны, но не экстенсионально эквивалентны: если в них подставить λx λy x
, первое преобразуется в λx λy x
, а второе зациклится.
Среди всевозможных лямбда-выражений есть несколько, играющих особо важную роль. О них и пойдёт речь в этом разделе.
Тождественным преобразованием называется выражение I = λx x
.
Генератором константы называется выражение K = λx λy x
.
Комбинатором подстановки называется выражение S = λa λb λx (a x) (b x)
.
Дупликатором называется выражение D = λx x x
. Иногда его ещё обозначают буквой M
.
Спаривателем называется выражение P = λa λb λc c a b
.
Композитором или ассоциатором называется выражение C = λf λg λx f (g x)
. Можно ещё встретить обозначение B
.
Транспозицией называется выражение T = λa λb b a
.
Это далеко не весь список часто встречающихся лямбда-выражений, но уже он достоин длительного обсуждения.
Начнём с важнейших комбинаторов: S
, K
, I
. Именно они находились в основе SKI-исчисления (М.Э. Шейнфинкель, начало 1920х), которое являлось непосредственным предком лямбда-исчисления. При помощи только этих трёх комбинаторов можно выразить любое лямбда-выражение. Алгоритм неожиданно прост: если обозначить {_}
соответствие между лямбда-выражениями и SKI-выражениями, то
{A B} = {A} {B}
{λ ... λ (λx x)} = {λ ... λ I}
{λ ... λ (λy x)} = {λ ... λ K x}
{λ ... λ (λx F G)} = {λ ... λ S {λx F} {λx G}}
Учитывая, что выражение SKK
преобразуется в I
, можно сделать вывод, что достаточно даже двух комбинаторов: S
и K
.
Остальные комбинаторы появятся в последующих разделах.
Есть традиционный для лямбда исчисления способ представления целых неотрицательных чисел:
<0> = λf λx x
<1> = λf λx f x
<2> = λf λx f (f x)
<3> = λf λx f (f (f x))
...
<n> = λf λx f (f ( ... (f x)..))
Грубо говоря, выражение <n>
применяет n
раз свой “первый” вход ко “второму”. Нетрудно видеть, что <n>
является результатом нормализации λf λx f (<n-1> f x)
, в связи с чем операцию “плюс один” определяют следующим образом:
(+1) = λn λf λx f (n f x)
Операцию сложения определить не сложнее:
(+) = λn n (+1)
Действительно:
(+)<m><n> → <m>(+1)<n> → (+1)((+1) ... ((+1)<n>)..) → ... → <n+m>
Умножение определяется похожим образом:
(*) = λm λn λf λx m (n f) x
или экстенсионально эквивалентным выражением
λm λn λf m (n f)
В лямбда-исчислении при использовании ленивого порядка вычислений условное ветвление реализуется почти тривиально. “Истина” и “ложь” кодируются выражениями
<true> = λa λb a = K
<false> = λa λb b = <0>
Само ветвление тогда реализуется выражением
<if> = λa λb λc a b с
Как видно из его определения, допустимы следующие преобразования:
<if> <true> A B → A
<if> <false> A B → B
При помощи некоторой модификации условной конструкции можно агрегировать данные. А именно, рассмотрим выражение
pair = λa λb λc <if> c a b
В него можно запихнуть два выражения A
и B
конструкцией pair A B
. Обратно их можно вытащить операциями
fst = λp p <true>
snd = λp p <false>
Стоит отметить, что выражение pair
эквивалентно комбинатору P
, упомянутому в одном из предыдущих разделов.
Как пример использования операции спаривания, покажем, как можно реализовать “псевдоминус один”, который от любого положительного числа отнимает 1, а ноль сохраняет:
shift = λp pair (snd p) ((+1) (snd p))
(-1) = λn fst (n shift (pair <0> <0>))
От полноценного (точнее, полного по Тюрингу) языка программирования нас отделяет только возможность реализации управляемого зацикливания. В лямбда-исчислении управляемое зацикливание реализуется как процедура поиска неподвижных точек.
Неподвижной точкой выражения F
называется выражение X
, в каком-то смысле эквивалентное F X
. Классический способ нахождения неподвижных точек – использование выражения
fix = λf (C f D) (C f D)
Действительно, есть следующая “цепочка” преобразований:
fix F → C F D (C F D) → F (D (C F D))
Последнее выражение нормально и экстенсионально эквивалентно F ((C F D) (C F D))
(хотя эквивалентность не совсем очевидна).
Более популярной альтернативой является комбинатор
Y = λf (λx f (x x)) (λx f (x x))
полученный из fix
путём нормализации внутренних (C f D)
. Популярность Y-комбинатора обусловлена следующей цепочкой преобразований:
Y F → (λx F (x x)) (λx F (x x)) → F ((λx F (x x)) (λx F (x x)))
Выражение X = (λx F (x x)) (λx F (x x))
не просто является неподвижной точкой операции F
, но ещё и преобразуется в F X
при ленивом порядке вычислений (для аппликативного порядка вычислений Y-комбинатор бессмысленен, вместо него используется т.н. Z-комбинатор).
Теперь на примере реализации факториала покажем, как можно использовать Y-комбинатор на практике. Как известно из курса арифметики:
fac = λn <if> ((=0) n) <1> (fac ((-1) n))
Реализация операции (-1)
приведена выше. Предикат (=0)
можно задать выражением
(=0) = λn n (K<false>) <true>
Остаётся проблема с рекуррентной зависимостью fac
от fac
. Решается она так:
fac_y = Y (λf λn <if> ((=0) n) <1> (f ((-1) n)))
Если через X
обозначить результат одного преобразования fac_y
, то X
экстенсионально эквивалентен выражению
λn <if> ((=0) n) <1> (X ((-1) n))
поэтому X
вычисляет факториал поданного ему на вход числа.
@ 2016 arbrk1, all rights reversed