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