Кевин лаконично указывает, как работает этот конкретный фрагмент кода (и почему он совершенно непонятен), но я хотел бы добавить некоторую информацию о том, как обычно работают батуты .
Без оптимизации хвостового вызова (TCO) каждый вызов функции добавляет кадр стека к текущему стеку выполнения. Предположим, у нас есть функция для вывода обратного отсчета чисел:
function countdown(n) {
if (n === 0) {
console.log("Blastoff!");
} else {
console.log("Launch in " + n);
countdown(n - 1);
}
}
Если мы позвоним countdown(3)
, давайте проанализируем, как будет выглядеть стек вызовов без TCO.
> countdown(3);
// stack: countdown(3)
Launch in 3
// stack: countdown(3), countdown(2)
Launch in 2
// stack: countdown(3), countdown(2), countdown(1)
Launch in 1
// stack: countdown(3), countdown(2), countdown(1), countdown(0)
Blastoff!
// returns, stack: countdown(3), countdown(2), countdown(1)
// returns, stack: countdown(3), countdown(2)
// returns, stack: countdown(3)
// returns, stack is empty
При использовании TCO каждый рекурсивный вызов countdown
находится в хвостовой позиции (ничего не остается, кроме как вернуть результат вызова), поэтому стековый кадр не выделяется. Без TCO стек взрывается даже при небольшом увеличении n
.
Trampolining обходит это ограничение, вставляя обертку вокруг countdown
функции. Затем countdown
не выполняет рекурсивные вызовы и вместо этого сразу возвращает функцию для вызова. Вот пример реализации:
function trampoline(firstHop) {
nextHop = firstHop();
while (nextHop) {
nextHop = nextHop()
}
}
function countdown(n) {
trampoline(() => countdownHop(n));
}
function countdownHop(n) {
if (n === 0) {
console.log("Blastoff!");
} else {
console.log("Launch in " + n);
return () => countdownHop(n-1);
}
}
Чтобы лучше понять, как это работает, давайте посмотрим на стек вызовов:
> countdown(3);
// stack: countdown(3)
// stack: countdown(3), trampoline
// stack: countdown(3), trampoline, countdownHop(3)
Launch in 3
// return next hop from countdownHop(3)
// stack: countdown(3), trampoline
// trampoline sees hop returned another hop function, calls it
// stack: countdown(3), trampoline, countdownHop(2)
Launch in 2
// stack: countdown(3), trampoline
// stack: countdown(3), trampoline, countdownHop(1)
Launch in 1
// stack: countdown(3), trampoline
// stack: countdown(3), trampoline, countdownHop(0)
Blastoff!
// stack: countdown(3), trampoline
// stack: countdown(3)
// stack is empty
На каждом шаге countdownHop
функция отказывается от прямого контроля за тем, что происходит дальше, вместо этого возвращая функцию для вызова, которая описывает, что она хотела бы, чтобы происходило дальше. Функция батута затем берет это и вызывает его, затем вызывает любую функцию, которая возвращает, и так далее, пока не будет «следующего шага». Это называется trampolining, потому что поток управления «подпрыгивает» между каждым рекурсивным вызовом и реализацией trampoline, а не функцией, непосредственно повторяющейся. Оставляя контроль над тем, кто делает рекурсивный вызов, функция батута может гарантировать, что стек не станет слишком большим. Примечание: эта реализация trampoline
опускает возвращаемые значения для простоты.
Может быть сложно узнать, хорошая ли это идея. Производительность может пострадать из-за каждого шага, выделяющего новое закрытие. Умные оптимизации могут сделать это жизнеспособным, но вы никогда не знаете. Прыжки на батуте в основном полезны для преодоления жестких ограничений рекурсии, например, когда языковая реализация устанавливает максимальный размер стека вызовов.
loopy
не переполняется, потому что не вызывает себя .