β-редукция

Переливание из пустого в порожнее продолжается, и сейчас поговорим о β-редукции. Именно не о конверсии, так как экспансии тут быть не может: есть только редукция. Вообще вся операционная семантика λ-исчисления — одно правило переписывания, не более чем.

Вызов функции в любом языке — частный случай β-редукции.

(λx.M)N  \displaystyle (\lambda x.\,M)\,N \; β  M[x:=N]\displaystyle \to_\beta\; M[x{:=}N]
(1.8)

Терм вида (λx.M)N(\lambda x.\,M)\,N называется редексом. Редукция может применяться к любому редексу в любом месте терма. Обозначение β\to_\beta означает один шаг β-редукции, β\twoheadrightarrow_\beta — ноль и более шагов, а =β=_\beta — эквивалентность, порождённая редукцией. Нормальная форма, или НФ, — это терм без редексов: вычислять больше нечего. Результатом работы программы как раз и должна быть нормальная форма.

Если запретить редукцию под знаком λ — не заглядывать в тело абстракции, — получается более слабое понятие: слабая нормальная форма. В ней стянуты все редексы, кроме спрятанных под λ. Скажем, λx.(λy.y)z\lambda x.\,(\lambda y.\,y)\,z уже в слабой нормальной форме, хотя обычной НФ λx.z\lambda x.\,z ещё не достиг. Именно до такой формы (точнее, слабой головной, WHNF) и вычисляют реальные языки: тело функции не трогают, пока её не позвали — подробнее в главе о стратегиях редукции.

Так в Haskell живут бесконечные структуры: их вычисляют до слабой головной формы, но не до конца.

nats = [1..]        -- бесконечный список 1 : 2 : 3 : …
-- уже в WHNF: виден внешний конструктор (:), хвост — невычисленный thunk;
-- полной НФ нет — чтобы её достичь, пришлось бы построить весь список
take 3 nats         -- [1,2,3]: берём только начало, и всё работает

Примеры β-редукций:

(λx.x)y\displaystyle (\lambda x.\,x)\,y βy\displaystyle \to_\beta y
(λf.f(fz))(λx.x)\displaystyle (\lambda f.\,f\,(f\,z))\,(\lambda x.\,x) β(λx.x)((λx.x)z)\displaystyle \to_\beta (\lambda x.\,x)\,((\lambda x.\,x)\,z) βz\displaystyle \twoheadrightarrow_\beta z

Но, как всегда, есть нюансы, и главный нюанс заключается в том, что далеко не все термы редуцируются до нормальной формы. Выше на Haskell я уже дал пример бесконечного списка, но тут будет не о нём. Терм Ω\Omega редуцируется сам в себя — вычисление не завершается никогда:

Ω\displaystyle \Omega =(λx.xx)(λx.xx)\displaystyle = (\lambda x.\,x\,x)\,(\lambda x.\,x\,x) βΩ\displaystyle \to_\beta \Omega β\displaystyle \to_\beta \cdots

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