Ich habe ein funktionierendes JavaScript erstellt. Ich hatte gedacht, dass die Tail-Call-Optimierung implementiert wurde, aber wie sich herausstellte, habe ich mich geirrt. So musste ich mir Trampolinspringen beibringen . Nachdem ich hier und anderswo ein bisschen gelesen hatte, konnte ich die Grundlagen erläutern und mein erstes Trampolin bauen:
/*not the fanciest, it's just meant to
reenforce that I know what I'm doing.*/
function loopy(x){
if (x<10000000){
return function(){
return loopy(x+1)
}
}else{
return x;
}
};
function trampoline(foo){
while(foo && typeof foo === 'function'){
foo = foo();
}
return foo;
/*I've seen trampolines without this,
mine wouldn't return anything unless
I had it though. Just goes to show I
only half know what I'm doing.*/
};
alert(trampoline(loopy(0)));
Mein größtes Problem ist, dass ich nicht weiß, warum das funktioniert. Ich habe die Idee, die Funktion in einer while-Schleife erneut auszuführen, anstatt eine rekursive Schleife zu verwenden. Ausgenommen, technisch gesehen hat meine Basisfunktion bereits eine rekursive Schleife. Ich führe die loopy
Basisfunktion nicht aus, aber ich führe die Funktion darin aus. Was hindert Sie foo = foo()
daran, einen Stapelüberlauf zu verursachen? Und ist foo = foo()
technisch nicht mutierend oder fehlt mir etwas? Vielleicht ist es nur ein notwendiges Übel. Oder eine Syntax, die mir fehlt.
Gibt es überhaupt eine Möglichkeit, das zu verstehen? Oder ist es nur ein Hack, der irgendwie funktioniert? Ich konnte alles andere durchstehen, aber dieser hat mich verwirrt.
loopy
läuft nicht über, weil es sich nicht selbst nennt .Antworten:
Der Grund, warum Ihr Gehirn gegen die Funktion rebelliert
loopy()
, ist der inkonsistente Typ :Viele Sprachen lassen Sie nicht einmal solche Dinge tun oder verlangen zumindest viel mehr Tipparbeit, um zu erklären, wie dies Sinn machen soll. Weil es das wirklich nicht tut. Funktionen und Ganzzahlen sind völlig unterschiedliche Arten von Objekten.
Lassen Sie uns diese while-Schleife sorgfältig durchgehen:
Anfangs
foo
ist gleichloopy(0)
. Was istloopy(0)
? Nun, es ist weniger als 10000000, also bekommen wirfunction(){return loopy(1)}
. Das ist ein wahrer Wert, und es ist eine Funktion, so dass die Schleife weitergeht.Nun kommen wir zu
foo = foo()
.foo()
ist das gleiche wieloopy(1)
. Da 1 immer noch kleiner als 10000000 ist, gibtfunction(){return loopy(2)}
dies zurück , dem wir dann zuweisenfoo
.foo
ist immer noch eine funktion, also machen wir weiter ... bis irgendwann foo gleich istfunction(){return loopy(10000000)}
. Das ist eine Funktion, also machen wir nochfoo = foo()
einmal, aber dieses Mal, wenn wir anrufenloopy(10000000)
, ist x nicht kleiner als 10000000, also bekommen wir einfach x zurück. Da 10000000 ebenfalls keine Funktion ist, wird auch die while-Schleife beendet.quelle
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:
Wenn wir anrufen
countdown(3)
, analysieren wir , wie der Aufrufstapel ohne TCO aussehen würde.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ügign
.Mit Trampolin wird diese Einschränkung umgangen, indem ein Wrapper um die
countdown
Funktion eingefügt wird . Führt danncountdown
keine rekursiven Aufrufe durch und gibt stattdessen sofort eine aufzurufende Funktion zurück. Hier ist eine Beispielimplementierung:Sehen wir uns den Aufrufstapel an, um einen besseren Überblick über die Funktionsweise zu erhalten:
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 dertrampoline
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.
quelle
Vielleicht wird es einfacher zu verstehen, wenn das Trampolin mit einem bestimmten Rückgabetyp implementiert ist (anstatt eine Funktion zu missbrauchen):
Vergleichen Sie dies mit Ihrer Version von
trampoline
, in der der Rekursionsfall ist, wenn die Funktion eine andere Funktion zurückgibt, und der Basisfall ist, wenn sie etwas anderes zurückgibt.Es nennt sich nicht mehr. Stattdessen wird ein Ergebnis (in meiner Implementierung buchstäblich a
Result
) zurückgegeben, das angibt, ob die Rekursion fortgesetzt oder abgebrochen werden soll.Ja, das ist genau das notwendige Übel der Schleife. Man könnte auch
trampoline
ohne Mutation schreiben , aber es würde eine erneute Rekursion erfordern:Trotzdem zeigt es die Idee, was die Trampolinfunktion noch besser macht.
Der Punkt des Trampolierens ist das Abstrahieren des schwanzrekursiven Aufrufs von der Funktion, die die Rekursion in einen Rückgabewert verwenden möchte, und das Ausführen der tatsächlichen Rekursion an nur einer Stelle - der
trampoline
Funktion, die dann an einer einzelnen Stelle für die Verwendung von a optimiert werden kann Schleife.quelle
foo = foo()
Dies ist eine Mutation im Sinne einer Änderung des lokalen Status, aber ich würde diese Neuzuweisung im Allgemeinen in Betracht ziehen, da Sie das zugrunde liegende Funktionsobjekt nicht ändern, sondern es durch die zurückgegebene Funktion (oder den zurückgegebenen Wert) ersetzen.foo
der die Variable enthält, geändert wird. Einewhile
Schleife benötigt einen veränderlichen Zustand, wenn sie beendet werden soll, in diesem Fall die Variablefoo
oderx
.fn
in einen rekursiven Aufruf von konvertiert.trampoline
Ich bin nicht sicher, ob dies eine Verbesserung darstellt.