Кевін лаконічно вказує, як працює саме цей фрагмент коду (разом із тим, чому це зовсім незрозуміло), але я хотів додати трохи інформації про те, як працюють батути взагалі .
Без оптимізації хвостових викликів (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
.
Трамполінінг долає це обмеження, вставляючи обгортку навколо 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
функція відмовляється від прямого управління тим, що буде далі, замість того, щоб повернути функцію виклику, яка описує, що вона хотіла б відбутися далі. Потім функція батута приймає це і викликає, потім викликає будь-яку функцію, яка повертається, і так далі, поки не буде "наступного кроку". Це називається батутним тренуванням, оскільки потік управління "відскакує" між кожним рекурсивним викликом та реалізацією батута, замість того, щоб функція безпосередньо повторювалася. Відмовившись від контролю того, хто здійснює рекурсивний дзвінок, функція батута може забезпечити, щоб стек не став занадто великим. Побічна примітка: ця реалізація trampoline
не дозволяє повертати значення для простоти.
Це може бути складно знати, чи це хороша ідея. Продуктивність може постраждати через кожен крок виділення нового закриття. Розумні оптимізації можуть зробити це життєздатним, але ви ніколи не знаєте. Трамполінінг в основному корисний для подолання жорстких обмежень рекурсії, наприклад, коли мовна реалізація встановлює максимальний розмір стека викликів.
loopy
не переповнює, тому що не називає себе .