Кевин лаконично указывает, как работает этот конкретный фрагмент кода (и почему он совершенно непонятен), но я хотел бы добавить некоторую информацию о том, как обычно работают батуты .
Без оптимизации хвостового вызова (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не переполняется, потому что не вызывает себя .