Осталась проблема, которой одинаково подвержены и комбинатор Карри, и комбинатор Тьюринга: в энергичном порядке (call-by-value) оба зацикливаются. Рекурсивный аргумент — сам или — среда раскручивает заранее, ещё до вызова функции, так и не дойдя до полезной работы. Исправить это можно, отложив рекурсивный вызов — превратив его в значение, которое раскроется лишь тогда, когда действительно понадобится.
Идея до боли простая: над самоаппликацией надо навесить обёртку-функцию. Технически это η-развёртка . Завёрнутый в вызов — уже значение и разворачивается только при реальном обращении. Так из комбинатора Карри (1.24) получается Z-комбинатор — та же конструкция, только каждое заменено на :
Проследим первый разворот. Обозначим ; тогда за два шага β-редукции:
Последнее равенство сворачивает обратно в . Главное — рекурсивный вызов вышел наружу завёрнутым в : под он остаётся значением и разворачивается лишь при реальном применении к аргументу (тогда как и в энергичном порядке разворачиваются сразу).
Тот же факториал, что и в разделах про Карри и Тьюринга (тот же шаблон , то же число ), считается настоящими β-редукциями, а завёрнутый вызов раскрывается ровно при переходе к следующему числу:
Поэтому под энергичным порядком (call-by-value) он не разворачивается заранее и не зацикливается — рекурсивный вызов срабатывает точно в нужный момент.
Ни Y, ни Θ, ни Z не типизируются в простом типизированном λ-исчислении: самоаппликация потребовала бы рекурсивного типа.
В Haskell такой рекурсивный тип и заводят — оборачивая самоаппликацию в newtype; на нём хорошо видно, что Z не опирается на лень. Обычный держится на ленивом узле , а Z строит рекурсию самоприменением и прячет рекурсивный вызов под — под значение. Заставим вычисление быть строгим (аргумент форсится перед применением функции, как при call-by-value): наивный Y зацикливается, а Z спокойно считает факториал:
newtype Rec a = Rec { unRec :: Rec a -> a }
-- $! форсит аргумент до применения f — это и есть энергичный порядок (call-by-value)
-- наивный Y: рекурсивный вызов (unRec x x) не значение, форсится заранее => зацикливается
yStrict :: (a -> a) -> a
yStrict f = w (Rec w) where w x = f $! unRec x x
-- Z: вызов завёрнут в (\v -> ...), это уже значение — $! его не разворачивает
zStrict :: ((b -> c) -> b -> c) -> b -> c
zStrict f = w (Rec w) where w x = f $! \v -> unRec x x v
fac :: (Int -> Int) -> Int -> Int
fac rec n = if n == 0 then 1 else n * rec (n - 1)
-- yStrict fac 5 => зацикливается
-- zStrict fac 5 => 120 -- Z досчитал без всякой ленивости