Kevin erklärt kurz und bündig, wie dieses Codefragment funktioniert (und warum es ziemlich unverständlich ist), aber ich wollte einige Informationen darüber hinzufügen, wie Trampoline im Allgemeinen funktionieren.
Ohne Tail-Call-Optimierung (TCO) fügt jeder Funktionsaufruf dem aktuellen Ausführungsstapel einen Stack-Frame hinzu . Angenommen, wir haben eine Funktion zum Ausdrucken eines Countdowns von Zahlen:
function countdown(n) {
if (n === 0) {
console.log("Blastoff!");
} else {
console.log("Launch in " + n);
countdown(n - 1);
}
}
Wenn wir anrufen countdown(3)
, analysieren wir , wie der Aufrufstapel ohne TCO aussehen würde.
> 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
Mit TCO befindet sich jeder rekursive Aufruf an countdown
in der Endposition (es bleibt nichts anderes zu tun, als das Ergebnis des Aufrufs zurückzugeben), sodass kein Stapelrahmen zugewiesen wird. Ohne TCO explodiert der Stapel sogar geringfügig n
.
Mit Trampolin wird diese Einschränkung umgangen, indem ein Wrapper um die countdown
Funktion eingefügt wird . Führt dann countdown
keine rekursiven Aufrufe durch und gibt stattdessen sofort eine aufzurufende Funktion zurück. Hier ist eine Beispielimplementierung:
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);
}
}
Sehen wir uns den Aufrufstapel an, um einen besseren Überblick über die Funktionsweise zu erhalten:
> 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
Bei jedem Schritt die countdownHop
Funktion verlässt direkte Kontrolle darüber , was als nächstes passiert, sondern eine Funktion der Rückkehr zu nennen das beschreibt , was es würde gerne nächstes passieren. Die Trampolin - Funktion nimmt dann diese und es nennt, dann ruft , was Funktion , die zurückgibt, und so weiter , bis es kein „nächster Schritt“ ist. Dies wird als Trampolin bezeichnet, da der Kontrollfluss zwischen jedem rekursiven Aufruf und der Trampolinimplementierung "springt", anstatt dass die Funktion direkt wiederholt wird. Durch den Verzicht Kontrolle darüber , wer macht den rekursiven Aufruf kann die Trampolin - Funktion Damit der Stapel nicht zu groß wird. Randnotiz: Bei dieser Implementierung werden der trampoline
Einfachheit halber keine Werte zurückgegeben.
Es kann schwierig sein zu wissen, ob dies eine gute Idee ist. Die Leistung kann durch jeden Schritt, der einen neuen Abschluss zuweist, beeinträchtigt werden. Clevere Optimierungen können dies möglich machen, aber Sie werden es nie erfahren. Trampolinieren ist vor allem nützlich, um harte Rekursionsgrenzen zu umgehen, beispielsweise wenn eine Sprachimplementierung eine maximale Call-Stack-Größe festlegt.
loopy
läuft nicht über, weil es sich nicht selbst nennt .