Kevin zwięźle wskazuje, jak działa ten konkretny fragment kodu (wraz z tym, dlaczego jest dość niezrozumiały), ale chciałem dodać trochę informacji o tym, jak ogólnie trampoliny działają.
Bez optymalizacji wywołania ogona (TCO) każde wywołanie funkcji dodaje ramkę stosu do bieżącego stosu wykonania. Załóżmy, że mamy funkcję do wydrukowania odliczania liczb:
function countdown(n) {
if (n === 0) {
console.log("Blastoff!");
} else {
console.log("Launch in " + n);
countdown(n - 1);
}
}
Jeśli zadzwonimy countdown(3)
, przeanalizujmy, jak wyglądałby stos wywołań bez całkowitego kosztu posiadania.
> 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
W przypadku TCO każde wywołanie rekurencyjne countdown
jest w pozycji ogonowej (nie pozostaje nic innego, jak zwrócić wynik wywołania), więc ramka stosu nie jest przydzielana. Bez całkowitego kosztu posiadania stos wysadza się nawet w przypadku bardzo dużych rozmiarów n
.
Trampolinowanie omija to ograniczenie, umieszczając opakowanie wokół countdown
funkcji. Następnie countdown
nie wykonuje wywołań rekurencyjnych i zamiast tego natychmiast zwraca funkcję do wywołania. Oto przykładowa implementacja:
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);
}
}
Aby lepiej zrozumieć, jak to działa, spójrzmy na stos wywołań:
> 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
Na każdym kroku countdownHop
funkcja porzuca bezpośrednią kontrolę, co dzieje się obok, zamiast wracać funkcję zadzwonić, który opisuje, co to lubią się wydarzy. Funkcja trampoliny bierze to i wywołuje, a następnie wywołuje dowolną funkcję, która powraca, i tak dalej, aż nie będzie „następnego kroku”. Nazywa się to trampolinowaniem, ponieważ przepływ sterowania „odbija się” między każdym wywołaniem rekurencyjnym a implementacją trampoliny, zamiast funkcji bezpośrednio powtarzającej się. Porzucając kontrolę nad tym, kto wykonuje wywołanie rekurencyjne, funkcja trampoliny może zapewnić, że stos nie będzie zbyt duży. Uwaga dodatkowa: ta implementacja trampoline
pomija zwracanie wartości dla uproszczenia.
Trudno jest ustalić, czy to dobry pomysł. Wydajność może ucierpieć z powodu każdego kroku przydzielającego nowe zamknięcie. Sprytne optymalizacje mogą sprawić, że będzie to wykonalne, ale nigdy nie wiadomo. Trampolinowanie jest szczególnie przydatne do omijania twardych limitów rekurencji, na przykład gdy implementacja języka ustawia maksymalny rozmiar stosu wywołań.
loopy
nie przepełnia się, ponieważ się nie nazywa .