Что делает рекурсию быстрой в функциональных языках, так это то, что компиляторы могут внутренне преобразовывать рекурсию в итерацию, используя удаление хвостовой рекурсии (или, в более общем смысле, удаление хвостовой рекурсии). По сути, если рекурсивный вызов является последней операцией перед возвратом функции, а возвращаемым значением функции является значение рекурсивного вызова, то вместо создания нового кадра стека программа будет повторно использовать текущий кадр. Переменные аргумента устанавливаются в новые значения, а ПК - в начало функции.
Использование преимущества устранения хвостовой рекурсии требует некоторой осведомленности программиста. Вы должны убедиться, что ваши рекурсивные вызовы на самом деле являются хвостовыми вызовами. Например, вот код в OCaml для вычисления факториала:
let rec factorial n =
if n = 0 then
1
else
n * factorial (n - 1)
Исключение хвостового вызова здесь не будет работать напрямую, так как умножение должно выполняться после рекурсивного вызова. Однако, если функция была переписана так:
let factorial n =
let rec fac_helper n p =
if n = 0 then
p
else
fac_helper (n - 1) (p * n)
in
fac_helper n 1
Теперь можно использовать устранение хвостовых вызовов. Это будет преобразовано в нечто подобное (в псевдокоде):
factorial p n =
p = 1
while n > 0
n = n - 1
p = p * n
return p
Этот стиль может показаться нелогичным, но он имеет такой же смысл, как и итеративная версия. Каждый шаг вычисления выполняется при вызове рекурсивной функции. Индукционные переменные, такие как p
и n
выше, которые используются во всем вычислении, объявляются в качестве аргументов.
Следует отметить, что большинство компиляторов как для императивных, так и для функциональных языков используют преимущества этой оптимизации. На самом деле, версия оптимизации LLVM даже допускает ассоциативные операции между рекурсивным вызовом и возвратом, так что вы можете написать первую версию факториала и при этом использовать оптимизацию. Однако устранение хвостовых вызовов в JVM не поддерживается, поэтому функциональные языки в JVM, такие как Scala, имеют ограниченную поддержку.