Стратегии редукции

Правило, по которому из всех редексов терма выбирается очередной, называется стратегией редукции. Разберёмся, где возникает выбор. Терм бывает трёх видов:

  • переменная xx — редуцировать нечего;
  • абстракция λx.M\lambda x.M — выбора нет: редуцируем тело MM;
  • аппликация M NM\,N — редексы могут быть и в левой части, и в правой; выбор между ними и делает стратегия.

Раскроем аппликацию: её левая часть может снова быть аппликацией, левая часть той — тоже, и так далее. Спустившись по левым ветвям до первой не-аппликации — до головы терма, — получим один из двух видов:

(…((x N1) N2)…Nk),\displaystyle (\dots((x\,N_1)\,N_2)\dots N_k),(…(((λx.M) N1) N2)…Nk).\displaystyle (\dots(((\lambda x.M)\,N_1)\,N_2)\dots N_k).

В первом виде голова — переменная xx: слева стягивать нечего, редексы могут прятаться только в аргументах NiN_i. Во втором голова — абстракция, и (λx.M) N1(\lambda x.M)\,N_1 — редекс; вот и развилка: стянуть его сразу или сначала вычислить аргумент. Этот выбор и различает две классические стратегии.

Нормальная стратегия стягивает самый левый внешний редекс — в нашей записи это (λx.M) N1(\lambda x.M)\,N_1: аргументы подставляются в тело невычисленными. Аппликативная стратегия поступает наоборот: сперва доводит аргумент редекса до нормальной формы и лишь потом стягивает сам редекс.

Терм удобно изображать деревом. Например, для ((λx.M) N1) N2((\lambda x.M)\,N_1)\,N_2 оно устроено так (рис. 1.4): узлы @@ — аппликации, узел λx\lambda x — абстракция. Самый левый внешний редекс на дереве ищется спуском от корня по левым рёбрам: это первый @@, у которого левый ребёнок — абстракция.

Дерево терма ((λx.M) N₁) N₂: два узла-аппликации @ и узел-абстракция λx; нижний @ — самый левый внешний редекс
Рис. 1.4. Дерево терма ((λx.M) N1) N2((\lambda x.M)\,N_1)\,N_2: узлы @@ — аппликации, узел λx\lambda x — абстракция; красный @@ — самый левый внешний редекс (λx.M) N1(\lambda x.M)\,N_1.

Этот же спуск позволяет ввести важное понятие — головную нормальную форму. Поясним его на двух примерах:

λx1…xn. y N1…Nk,\displaystyle \lambda x_1\dots x_n.\,y\,N_1\dots N_k,​n,k\displaystyle n,k​≥0,\displaystyle {} \ge 0,λx1…xn. (λz.M) N1…Nk,\displaystyle \lambda x_1\dots x_n.\,(\lambda z.M)\,N_1\dots N_k,​n\displaystyle n​≥0, k\displaystyle {} \ge 0,\ k​>0.\displaystyle {} >0.

Первый терм — головная нормальная форма: спуск по левым ветвям упирается в переменную yy, она называется головной переменной. Голова такого терма уже не изменится, редексы могут прятаться лишь в аргументах NiN_i. Второй терм головной нормальной формой не является: его голова — редекс (λz.M) N1(\lambda z.M)\,N_1, он называется головным редексом.

Сформулируем обе стратегии точно — правилами операционной семантики. Мало нам понятий — надо ещё: понадобятся три синтаксические категории. Первая — не-абстракция NA\mathit{NA}:

NA::=x∣M N.\mathit{NA} \mathrel{\text{::=}} x \mid M\,N.

Тут, кажется, всё понятно: это любой терм, только не абстракция. MM и NN — произвольные термы, так что не-абстракция вполне может быть редексом — например, (λy.y) z(\lambda y.y)\,z. Две другие категории описывают готовые результаты: нормальные формы NF\mathit{NF} и их подкласс — нормальные формы-не-абстракции NANF\mathit{NANF}:

NF::=λx.NF∣NANF,\displaystyle \mathit{NF} \mathrel{\text{::=}} \lambda x.\mathit{NF} \mid \mathit{NANF},NANF::=x∣NANF NF.\displaystyle \mathit{NANF} \mathrel{\text{::=}} x \mid \mathit{NANF}\ \mathit{NF}.

Разделение необходимо: без него была бы возможна аппликация, у которой слева стоит абстракция, — а такой терм уже не нормальная форма, это редекс. Примеры NF\mathit{NF}: λx. x\lambda x.\,x, λf.λx. f x\lambda f.\lambda x.\,f\,x. Примеры NANF\mathit{NANF}: xx, f xf\,x, x (λy.y) zx\,(\lambda y.y)\,z — причём каждый из них одновременно и NF\mathit{NF}.

Теперь наконец-то стратегии. В основе каждой лежит своё правило стягивания редекса. Нормальная стратегия использует обычную β\beta-редукцию: редекс стягивается сразу, аргумент NN — произвольный терм, он подставляется в тело невычисленным:

(λx.M) N\displaystyle (\lambda x.M)\,N →M[x ⁣:= ⁣N]\displaystyle {} \to M[x\!:=\!N]
(1.33)

Аппликативная стратегия стягивает редекс иначе — лишь когда аргумент уже вычислен, то есть доведён до нормальной формы — слева от стрелки стоит NF\mathit{NF}, и пока аргумент не таков, стягивание запрещено:

(λx.M) NF\displaystyle (\lambda x.M)\,\mathit{NF} →M[x ⁣:= ⁣NF]\displaystyle {} \to M[x\!:=\!\mathit{NF}]
(1.34)

Так как мы не можем стянуть редекс, пока аргумент не в нормальной форме, в аппликативной стратегии есть правило для аргумента:

N→N′(λx.M) N→(λx.M) N′\dfrac{N \to N'}{(\lambda x.M)\,N \to (\lambda x.M)\,N'}
(1.35)

Оставшиеся правила — общие для нормальной стратегии и аппликативной:

NA→NA′NA N→NA′ N,\displaystyle \dfrac{\mathit{NA} \to \mathit{NA}'}{\mathit{NA}\,N \to \mathit{NA}'\,N},​N→N′NANF N→NANF N′,\displaystyle \dfrac{N \to N'}{\mathit{NANF}\,N \to \mathit{NANF}\,N'},​M→M′λx.M→λx.M′\displaystyle \dfrac{M \to M'}{\lambda x.M \to \lambda x.M'}
(1.36)

Первое правило редуцирует левую часть аппликации, пока та остаётся не-абстракцией; второе переходит к аргументу, лишь когда слева уже стоит NANF\mathit{NANF}; третье заводит редукцию в тело абстракции. Категории NA\mathit{NA} и NANF\mathit{NANF} в посылках как раз и гарантируют порядок «самый левый внешний»: пока слева есть что стягивать, вправо не смотрим.

Выбор самого левого внешнего редекса неслучаен: теорема о нормализации (Карри) гарантирует, что если у терма есть нормальная форма, то последовательное стягивание именно такого редекса её достигнет. За это нормальную стратегию называют полной.

Полнота не бесплатна: аргумент, подставленный в несколько мест, приходится пересчитывать. Для «большого» NN: (λx. F x (G x) x) N(\lambda x.\,F\,x\,(G\,x)\,x)\,N​→βF N (G N) N{} \to_\beta F\,N\,(G\,N)\,N — здесь NN придётся редуцировать трижды — по разу на копию. Аппликативная стратегия вычислит NN ровно один раз, но платит за это полнотой. Пусть K\mathrm{K}​≡λxy.x{} \equiv \lambda x y.x, I\mathrm{I}​≡λx.x{} \equiv \lambda x.x, а Ω\Omega​≡(λx.x x) (λx.x x){} \equiv (\lambda x.x\,x)\,(\lambda x.x\,x) — терм без нормальной формы. Нормальная стратегия вычисляет K I Ω\mathrm{K}\,\mathrm{I}\,\Omega​↠βI{} \twoheadrightarrow_\beta \mathrm{I}, отбросив Ω\Omega не глядя; аппликативная же сперва берётся за аргумент Ω\Omega — и зацикливается.

В языках программирования обе стратегии живут под другими именами. Аппликативная — это вызов по значению (call-by-value) строгих языков вроде Си: сначала аргументы, потом применение. Нормальная — основа ленивых вроде Haskell: вызов по имени (call-by-name).