Als ich anfing, Lisp zu lernen, bin ich auf den Begriff Schwanzrekursiv gestoßen . Was bedeutet es genau?
1694
Als ich anfing, Lisp zu lernen, bin ich auf den Begriff Schwanzrekursiv gestoßen . Was bedeutet es genau?
Antworten:
Stellen Sie sich eine einfache Funktion vor, die die ersten N natürlichen Zahlen addiert. (zB
sum(5) = 1 + 2 + 3 + 4 + 5 = 15
).Hier ist eine einfache JavaScript-Implementierung, die Rekursion verwendet:
Wenn Sie anrufen
recsum(5)
, bewertet der JavaScript-Interpreter Folgendes:Beachten Sie, wie jeder rekursive Aufruf abgeschlossen sein muss, bevor der JavaScript-Interpreter tatsächlich mit der Berechnung der Summe beginnt.
Hier ist eine rekursive Version derselben Funktion:
Hier ist die Abfolge von Ereignissen, die auftreten würden, wenn Sie aufrufen
tailrecsum(5)
würden (wastailrecsum(5, 0)
aufgrund des zweiten Standardarguments effektiv wäre ).Im Schwanz-rekursiven Fall wird bei jeder Auswertung des rekursiven Aufrufs das
running_total
aktualisiert.Hinweis: In der ursprünglichen Antwort wurden Beispiele aus Python verwendet. Diese wurden in JavaScript geändert, da Python-Interpreter die Tail-Call-Optimierung nicht unterstützen . Während die Tail-Call-Optimierung Teil der ECMAScript 2015-Spezifikation ist , unterstützen sie die meisten JavaScript-Interpreter nicht .
quelle
tail recursion
dies in einer Sprache erreicht werden kann, die Tail Calls nicht optimiert.Bei der herkömmlichen Rekursion besteht das typische Modell darin, dass Sie zuerst Ihre rekursiven Aufrufe ausführen und dann den Rückgabewert des rekursiven Aufrufs verwenden und das Ergebnis berechnen. Auf diese Weise erhalten Sie das Ergebnis Ihrer Berechnung erst, wenn Sie von jedem rekursiven Aufruf zurückgekehrt sind.
Bei der Endrekursion führen Sie zuerst Ihre Berechnungen durch und führen dann den rekursiven Aufruf aus, wobei Sie die Ergebnisse Ihres aktuellen Schritts an den nächsten rekursiven Schritt übergeben. Dies führt dazu, dass die letzte Aussage die Form von hat
(return (recursive-function params))
. Grundsätzlich ist der Rückgabewert eines bestimmten rekursiven Schritts der gleiche wie der Rückgabewert des nächsten rekursiven Aufrufs .Dies hat zur Folge, dass Sie den aktuellen Stapelrahmen nicht mehr benötigen, sobald Sie bereit sind, Ihren nächsten rekursiven Schritt auszuführen. Dies ermöglicht eine gewisse Optimierung. Tatsächlich sollten Sie mit einem entsprechend geschriebenen Compiler niemals einen Stapelüberlauf- Snicker mit einem rekursiven Schwanzaufruf haben. Verwenden Sie einfach den aktuellen Stapelrahmen für den nächsten rekursiven Schritt. Ich bin mir ziemlich sicher, dass Lisp das tut.
quelle
Ein wichtiger Punkt ist, dass die Schwanzrekursion im Wesentlichen einer Schleife entspricht. Es geht nicht nur um die Optimierung des Compilers, sondern auch um die Ausdruckskraft. Dies geht in beide Richtungen: Sie können eine beliebige Schleife des Formulars nehmen
wo
E
undQ
sind Ausdrücke undS
ist eine Folge von Anweisungen, und verwandeln Sie es in eine rekursive SchwanzfunktionNatürlich
E
,S
undQ
müssen definiert werden einige interessante Wert über einige Variablen zu berechnen. Zum Beispiel die Schleifenfunktionentspricht der (den) rekursiven Funktion (en)
(Dieses "Umschließen" der Schwanzrekursionsfunktion mit einer Funktion mit weniger Parametern ist eine übliche Funktionssprache.)
quelle
else { return k; }
kann geändert werden zureturn k;
Dieser Auszug aus dem Buch Programmieren in Lua zeigt, wie man eine richtige Schwanzrekursion macht (in Lua, sollte aber auch für Lisp gelten) und warum es besser ist.
Sie sehen also, wenn Sie einen rekursiven Aufruf tätigen wie:
Dies ist nicht rekursiv, da Sie in dieser Funktion nach dem rekursiven Aufruf noch etwas zu tun haben (1 hinzufügen). Wenn Sie eine sehr hohe Zahl eingeben, führt dies wahrscheinlich zu einem Stapelüberlauf.
quelle
Bei Verwendung der regulären Rekursion schiebt jeder rekursive Aufruf einen anderen Eintrag auf den Aufrufstapel. Wenn die Rekursion abgeschlossen ist, muss die App jeden Eintrag ganz nach unten entfernen.
Bei der Endrekursion kann der Compiler je nach Sprache den Stapel möglicherweise auf einen Eintrag reduzieren, sodass Sie Speicherplatz sparen ... Eine große rekursive Abfrage kann tatsächlich einen Stapelüberlauf verursachen.
Grundsätzlich können Schwanzrekursionen in Iterationen optimiert werden.
quelle
Die Jargon-Datei enthält Folgendes zur Definition der Schwanzrekursion:
Schwanzrekursion /n./
Wenn Sie es noch nicht satt haben, sehen Sie sich die Schwanzrekursion an.
quelle
Anstatt es mit Worten zu erklären, hier ein Beispiel. Dies ist eine Schemaversion der Fakultätsfunktion:
Hier ist eine Version von Fakultät, die schwanzrekursiv ist:
Sie werden in der ersten Version feststellen, dass der rekursive Aufruf von fact in den Multiplikationsausdruck eingespeist wird und daher der Status beim Ausführen des rekursiven Aufrufs auf dem Stapel gespeichert werden muss. In der rekursiven Version wartet kein anderer S-Ausdruck auf den Wert des rekursiven Aufrufs, und da keine weitere Arbeit zu erledigen ist, muss der Status nicht auf dem Stapel gespeichert werden. In der Regel verwenden schema-rekursive Funktionen einen konstanten Stapelraum.
quelle
list-reverse
wird in einem konstanten Stapelspeicher ausgeführt, erstellt und erweitert jedoch eine Datenstruktur auf dem Heap. Eine Baumdurchquerung könnte in einem zusätzlichen Argument einen simulierten Stapel verwenden. usw.Die Schwanzrekursion bezieht sich darauf, dass der rekursive Aufruf im letzten Logikbefehl im rekursiven Algorithmus der letzte ist.
In der Regel haben Sie bei der Rekursion einen Basisfall, der die rekursiven Aufrufe stoppt und den Aufrufstapel öffnet. Um ein klassisches Beispiel zu verwenden, obwohl mehr C-ish als Lisp, veranschaulicht die Fakultätsfunktion die Schwanzrekursion. Der rekursive Aufruf erfolgt nach Überprüfung der Basisfallbedingung.
Der erste Aufruf von Fakultät wäre
factorial(n)
wofac=1
(Standardwert) und n ist die Zahl, für die die Fakultät berechnet werden soll.quelle
else
ist der Schritt, den Sie als "Basisfall" bezeichnen können, der sich jedoch über mehrere Zeilen erstreckt. Verstehe ich Sie falsch oder ist meine Annahme richtig? Schwanzrekursion ist nur für einen Liner gut?factorial
Beispiel ist nur das klassische einfache Beispiel, das ist alles.Dies bedeutet, dass Sie nicht den Befehlszeiger auf dem Stapel drücken müssen, sondern einfach an den Anfang einer rekursiven Funktion springen und die Ausführung fortsetzen können. Dadurch können Funktionen unbegrenzt wiederholt werden, ohne dass der Stapel überläuft.
Ich habe einen Blog- Beitrag zu diesem Thema geschrieben, der grafische Beispiele dafür enthält, wie die Stapelrahmen aussehen.
quelle
Hier ist ein kurzer Code-Ausschnitt, der zwei Funktionen vergleicht. Die erste ist die traditionelle Rekursion zum Ermitteln der Fakultät einer bestimmten Zahl. Die zweite verwendet die Schwanzrekursion.
Sehr einfach und intuitiv zu verstehen.
Eine einfache Methode, um festzustellen, ob eine rekursive Funktion eine rekursive Endfunktion ist, besteht darin, dass sie im Basisfall einen konkreten Wert zurückgibt. Das bedeutet, dass es nicht 1 oder true oder ähnliches zurückgibt. Es wird höchstwahrscheinlich eine Variante eines der Methodenparameter zurückgeben.
Eine andere Möglichkeit besteht darin, festzustellen, ob der rekursive Aufruf frei von Additionen, Arithmetik, Änderungen usw. ist. Dies bedeutet, dass es sich nur um einen reinen rekursiven Aufruf handelt.
quelle
Der beste Weg für mich zu verstehen
tail call recursion
ist ein Sonderfall der Rekursion, wo der letzte Anruf (oder der Endaufruf) die Funktion selbst ist.Vergleich der in Python bereitgestellten Beispiele:
^ Rekursion
^ SCHWANZREKURSION
Wie Sie in der allgemeinen rekursiven Version sehen können, ist der letzte Aufruf im Codeblock
x + recsum(x - 1)
. Nach dem Aufruf derrecsum
Methode gibt es also eine andere Operation:x + ..
:In der rekursiven Endversion ist jedoch der letzte Aufruf (oder der Endaufruf) im Codeblock
tailrecsum(x - 1, running_total + x)
der letzte Aufruf der Methode selbst und keine Operation danach.Dieser Punkt ist wichtig, da die Tail-Rekursion, wie hier gezeigt, den Speicher nicht wachsen lässt, da der aktuelle Stapelrahmen eliminiert wird, wenn die zugrunde liegende VM sieht, dass sich eine Funktion an einer Tail-Position aufruft (der letzte Ausdruck, der in einer Funktion ausgewertet wird) wird als Tail Call Optimization (TCO) bezeichnet.
BEARBEITEN
NB. Beachten Sie, dass das obige Beispiel in Python geschrieben ist, dessen Laufzeit TCO nicht unterstützt. Dies ist nur ein Beispiel, um den Punkt zu erklären. TCO wird in Sprachen wie Scheme, Haskell usw. Unterstützt
quelle
In Java ist hier eine mögliche rekursive Implementierung der Fibonacci-Funktion:
Vergleichen Sie dies mit der rekursiven Standardimplementierung:
quelle
iter
vonacc
wann abgezogen wirditer < (n-1)
.Ich bin kein Lisp-Programmierer, aber ich denke, das wird helfen.
Grundsätzlich ist es ein Programmierstil, bei dem der rekursive Aufruf das letzte ist, was Sie tun.
quelle
Hier ist ein Common Lisp-Beispiel, das Fakultäten mithilfe der Schwanzrekursion ausführt. Aufgrund der stapellosen Natur könnte man wahnsinnig große faktorielle Berechnungen durchführen ...
Und dann könnten Sie es zum Spaß versuchen
(format nil "~R" (! 25))
quelle
Kurz gesagt, eine Schwanzrekursion hat den rekursiven Aufruf als letzte Anweisung in der Funktion, damit sie nicht auf den rekursiven Aufruf warten muss.
Dies ist also eine Schwanzrekursion, dh N (x - 1, p * x) ist die letzte Anweisung in der Funktion, bei der der Compiler klug ist, herauszufinden, dass sie für eine for-Schleife (Fakultät) optimiert werden kann. Der zweite Parameter p trägt den Zwischenproduktwert.
Dies ist die nicht schwanzrekursive Art, die obige Fakultätsfunktion zu schreiben (obwohl einige C ++ - Compiler sie möglicherweise trotzdem optimieren können).
aber das ist nicht:
Ich habe einen langen Beitrag mit dem Titel " Grundlegendes zur Schwanzrekursion - Visual Studio C ++ - Assembly View " geschrieben.
quelle
Hier ist eine Perl 5-Version der
tailrecsum
zuvor erwähnten Funktion.quelle
Dies ist ein Auszug aus der Struktur und Interpretation von Computerprogrammen über die Schwanzrekursion.
quelle
Die rekursive Funktion ist eine Funktion, die von selbst aufruft
Es ermöglicht Programmierern, effiziente Programme mit einer minimalen Menge an Code zu schreiben .
Der Nachteil ist, dass sie Endlosschleifen und andere unerwartete Ergebnisse verursachen können, wenn sie nicht richtig geschrieben werden .
Ich werde sowohl die einfache rekursive Funktion als auch die Schwanzrekursive Funktion erklären
Um eine einfache rekursive Funktion zu schreiben
Aus dem gegebenen Beispiel:
Aus dem obigen Beispiel
Ist der entscheidende Faktor, wann die Schleife verlassen werden soll
Ist die eigentliche Verarbeitung durchzuführen
Lassen Sie mich die Aufgabe einzeln aufteilen, um das Verständnis zu erleichtern.
Lassen Sie uns sehen, was intern passiert, wenn ich renne
fact(4)
If
Die Schleife schlägt fehl und geht zurelse
Schleife, sodass sie zurückkehrt4 * fact(3)
Im Stapelspeicher haben wir
4 * fact(3)
Einsetzen von n = 3
If
Die Schleife schlägt fehl und geht zurelse
Schleifeso kehrt es zurück
3 * fact(2)
Denken Sie daran, wir haben `` `4 * fact (3)` `genannt
Die Ausgabe für
fact(3) = 3 * fact(2)
Soweit hat der Stack
4 * fact(3) = 4 * 3 * fact(2)
Im Stapelspeicher haben wir
4 * 3 * fact(2)
Einsetzen von n = 2
If
Die Schleife schlägt fehl und geht zurelse
Schleifeso kehrt es zurück
2 * fact(1)
Denken Sie daran, wir haben angerufen
4 * 3 * fact(2)
Die Ausgabe für
fact(2) = 2 * fact(1)
Soweit hat der Stack
4 * 3 * fact(2) = 4 * 3 * 2 * fact(1)
Im Stapelspeicher haben wir
4 * 3 * 2 * fact(1)
Einsetzen von n = 1
If
Schleife ist wahrso kehrt es zurück
1
Denken Sie daran, wir haben angerufen
4 * 3 * 2 * fact(1)
Die Ausgabe für
fact(1) = 1
Soweit hat der Stack
4 * 3 * 2 * fact(1) = 4 * 3 * 2 * 1
Schließlich ist das Ergebnis von Tatsache (4) = 4 · 3 · 2 · 1 = 24
Die Schwanzrekursion wäre
If
Die Schleife schlägt fehl und geht zurelse
Schleife, sodass sie zurückkehrtfact(3, 4)
Im Stapelspeicher haben wir
fact(3, 4)
Einsetzen von n = 3
If
Die Schleife schlägt fehl und geht zurelse
Schleifeso kehrt es zurück
fact(2, 12)
Im Stapelspeicher haben wir
fact(2, 12)
Einsetzen von n = 2
If
Die Schleife schlägt fehl und geht zurelse
Schleifeso kehrt es zurück
fact(1, 24)
Im Stapelspeicher haben wir
fact(1, 24)
Einsetzen von n = 1
If
Schleife ist wahrso kehrt es zurück
running_total
Die Ausgabe für
running_total = 24
Schließlich ist das Ergebnis der Tatsache (4,1) = 24
quelle
Schwanzrekursion ist das Leben, das Sie gerade leben. Sie recyceln ständig denselben Stapelrahmen immer wieder, da es keinen Grund oder keine Möglichkeit gibt, zu einem "vorherigen" Rahmen zurückzukehren. Die Vergangenheit ist vorbei und erledigt, so dass sie verworfen werden kann. Sie erhalten einen Frame, der sich für immer in die Zukunft bewegt, bis Ihr Prozess unweigerlich zum Erliegen kommt.
Die Analogie bricht zusammen, wenn Sie bedenken, dass einige Prozesse möglicherweise zusätzliche Frames verwenden, aber dennoch als schwanzrekursiv betrachtet werden, wenn der Stapel nicht unendlich wächst.
quelle
Betrachten Sie das Problem der Berechnung der Fakultät einer Zahl.
Ein einfacher Ansatz wäre:
Angenommen, Sie nennen Fakultät (4). Der Rekursionsbaum wäre:
Die maximale Rekursionstiefe im obigen Fall beträgt O (n).
Betrachten Sie jedoch das folgende Beispiel:
Rekursionsbaum für factTail (4) wäre:
Auch hier beträgt die maximale Rekursionstiefe O (n), aber keiner der Aufrufe fügt dem Stapel eine zusätzliche Variable hinzu. Daher kann der Compiler auf einen Stapel verzichten.
quelle
Die Schwanzrekursion ist im Vergleich zur normalen Rekursion ziemlich schnell. Dies ist schnell, da die Ausgabe des Ahnenaufrufs nicht im Stapel geschrieben wird, um den Überblick zu behalten. Bei normaler Rekursion rufen alle Vorfahren die im Stapel geschriebene Ausgabe auf, um den Überblick zu behalten.
quelle
Eine Schwanzrekursionsfunktion ist eine rekursive Funktion, bei der die letzte Operation, die sie vor der Rückkehr ausführt, der rekursive Funktionsaufruf ist. Das heißt, der Rückgabewert des rekursiven Funktionsaufrufs wird sofort zurückgegeben. Zum Beispiel würde Ihr Code folgendermaßen aussehen:
Compiler und Interpreter, die die Tail-Call-Optimierung oder die Tail-Call-Eliminierung implementieren , können rekursiven Code optimieren, um Stapelüberläufe zu verhindern. Wenn Ihr Compiler oder Interpreter keine Tail-Call-Optimierung implementiert (wie z. B. der CPython-Interpreter), bietet das Schreiben Ihres Codes auf diese Weise keinen zusätzlichen Vorteil.
Dies ist beispielsweise eine standardmäßige rekursive Fakultätsfunktion in Python:
Und dies ist eine rekursive Tail-Call-Version der Fakultätsfunktion:
(Beachten Sie, dass der CPython-Interpreter, obwohl dies Python-Code ist, keine Tail-Call-Optimierung durchführt. Wenn Sie Ihren Code so anordnen, hat dies keinen Laufzeitvorteil.)
Möglicherweise müssen Sie Ihren Code etwas unleserlicher machen, um die Tail-Call-Optimierung nutzen zu können, wie im faktoriellen Beispiel gezeigt. (Zum Beispiel ist der Basisfall jetzt etwas unintuitiv und der
accumulator
Parameter wird effektiv als eine Art globale Variable verwendet.)Der Vorteil der Tail-Call-Optimierung besteht jedoch darin, dass Stapelüberlauffehler vermieden werden. (Ich werde bemerken, dass Sie denselben Vorteil erzielen können, indem Sie einen iterativen Algorithmus anstelle eines rekursiven verwenden.)
Stapelüberläufe werden verursacht, wenn auf den Aufrufstapel zu viele Frame-Objekte verschoben wurden. Ein Frame-Objekt wird beim Aufruf einer Funktion auf den Aufrufstapel verschoben und beim Zurückkehren der Funktion vom Aufrufstapel entfernt. Rahmenobjekte enthalten Informationen wie lokale Variablen und welche Codezeile bei der Rückkehr der Funktion zurückgegeben werden soll.
Wenn Ihre rekursive Funktion zu viele rekursive Aufrufe ohne Rückgabe ausführt, kann der Aufrufstapel sein Rahmenobjektlimit überschreiten. (Die Anzahl variiert je nach Plattform. In Python sind es standardmäßig 1000 Frame-Objekte.) Dies führt zu einem Stapelüberlauffehler . (Hey, daher kommt der Name dieser Website!)
Wenn Ihre rekursive Funktion jedoch als letztes den rekursiven Aufruf ausführt und seinen Rückgabewert zurückgibt, gibt es keinen Grund, das aktuelle Frame-Objekt auf dem Aufrufstapel zu belassen. Wenn nach dem rekursiven Funktionsaufruf kein Code vorhanden ist, gibt es keinen Grund, an den lokalen Variablen des aktuellen Frame-Objekts festzuhalten. So können wir das aktuelle Frame-Objekt sofort entfernen, anstatt es auf dem Aufrufstapel zu belassen. Das Endergebnis davon ist, dass Ihr Aufrufstapel nicht größer wird und daher keinen Stapelüberlauf verursachen kann.
Ein Compiler oder Interpreter muss über eine Tail-Call-Optimierung verfügen, damit er erkennen kann, wann die Tail-Call-Optimierung angewendet werden kann. Selbst dann haben Sie möglicherweise den Code in Ihrer rekursiven Funktion neu angeordnet, um die Tail-Call-Optimierung zu nutzen, und es liegt an Ihnen, ob diese potenzielle Verringerung der Lesbarkeit die Optimierung wert ist.
quelle
Um einige der Hauptunterschiede zwischen Tail-Call-Rekursion und Nicht-Tail-Call-Rekursion zu verstehen, können wir die .NET-Implementierungen dieser Techniken untersuchen.
Hier ist ein Artikel mit einigen Beispielen in C #, F # und C ++ \ CLI: Abenteuer in der Schwanzrekursion in C #, F # und C ++ \ CLI .
C # optimiert nicht für die Tail-Call-Rekursion, während F # dies tut.
Die prinzipiellen Unterschiede betreffen Schleifen gegenüber der Lambda-Rechnung. C # wurde unter Berücksichtigung von Schleifen entworfen, während F # aus den Prinzipien der Lambda-Rechnung aufgebaut ist. Ein sehr gutes (und kostenloses) Buch über die Prinzipien der Lambda-Rechnung finden Sie unter Struktur und Interpretation von Computerprogrammen von Abelson, Sussman und Sussman .
In Bezug auf Tail Calls in F # finden Sie einen sehr guten Einführungsartikel unter Detaillierte Einführung in Tail Calls in F # . Schließlich ist hier ein Artikel, der den Unterschied zwischen Nicht-Schwanz-Rekursion und Schwanz-Anruf-Rekursion (in F #) behandelt: Schwanz-Rekursion vs. Nicht-Schwanz-Rekursion in Fis .
Wenn Sie mehr über die Designunterschiede der Tail-Call-Rekursion zwischen C # und F # erfahren möchten, lesen Sie Generieren des Tail-Call-Opcodes in C # und F # .
Wenn Sie wissen möchten, unter welchen Bedingungen der C # -Compiler Tail-Call-Optimierungen durchführen kann, lesen Sie diesen Artikel: JIT CLR-Tail-Call-Bedingungen .
quelle
Es gibt zwei grundlegende Arten von Rekursionen: Kopfrekursion und Schwanzrekursion.
Entnommen aus diesem super tollen Beitrag. Bitte lesen Sie es.
quelle
Rekursion bedeutet eine Funktion, die sich selbst aufruft. Zum Beispiel:
Schwanzrekursion bedeutet die Rekursion, die die Funktion abschließt:
Sehen Sie, das Letzte, was eine unendliche Funktion (Prozedur im Schema-Jargon) tut, ist, sich selbst aufzurufen. Ein weiteres (nützlicheres) Beispiel ist:
In der Helper-Prozedur ist das LETZTE, was es tut, wenn die Linke nicht Null ist, sich selbst aufzurufen (NACH Nachteile von etwas und cdr etwas). So ordnen Sie im Grunde eine Liste zu.
Die Schwanzrekursion hat den großen Vorteil, dass der Interpreter (oder Compiler, abhängig von Sprache und Hersteller) sie optimieren und in etwas umwandeln kann, das einer while-Schleife entspricht. Tatsächlich wird in der Schematradition die meiste "for" - und "while" -Schleife in einer Schwanzrekursionsmethode durchgeführt (meines Wissens gibt es keine for and while).
quelle
Diese Frage hat viele gute Antworten ... aber ich kann nicht anders, als mich mit einer alternativen Sichtweise zu beschäftigen, wie man "Schwanzrekursion" oder zumindest "richtige Schwanzrekursion" definiert. Nämlich: sollte man es als eine Eigenschaft eines bestimmten Ausdrucks in einem Programm betrachten? Oder sollte man es als eine Eigenschaft einer Implementierung einer Programmiersprache betrachten ?
Für mehr über die letztere Ansicht gibt es ein klassisches Papier von Will Clinger, "Proper Tail Recursion and Space Efficiency" (PLDI 1998), das "Proper Tail Recursion" als eine Eigenschaft einer Programmiersprachenimplementierung definiert. Die Definition ist so aufgebaut, dass Implementierungsdetails ignoriert werden können (z. B. ob der Aufrufstapel tatsächlich über den Laufzeitstapel oder über eine vom Heap zugewiesene verknüpfte Liste von Frames dargestellt wird).
Um dies zu erreichen, wird eine asymptotische Analyse verwendet: nicht die Programmausführungszeit, wie man normalerweise sieht, sondern die Nutzung des Programmraums . Auf diese Weise ist die Speicherplatznutzung einer Heap-zugewiesenen verknüpften Liste im Vergleich zu einem Laufzeitaufrufstapel asymptotisch äquivalent. Man kann also dieses Implementierungsdetail der Programmiersprache ignorieren (ein Detail, das in der Praxis sicherlich ziemlich wichtig ist, aber das Wasser ziemlich trüben kann, wenn man versucht festzustellen, ob eine bestimmte Implementierung die Anforderung erfüllt, "rekursiv für Eigenschaftsschwänze" zu sein). )
Das Papier ist aus mehreren Gründen eine sorgfältige Untersuchung wert:
Es gibt eine induktive Definition der Tail-Ausdrücke und Tail-Aufrufe eines Programms. (Eine solche Definition und warum solche Anrufe wichtig sind, scheint Gegenstand der meisten anderen hier gegebenen Antworten zu sein.)
Hier sind diese Definitionen, um einen Eindruck vom Text zu vermitteln:
(Ein rekursiver Tail-Aufruf oder, wie in der Veröffentlichung angegeben, "Self-Tail-Aufruf" ist ein Sonderfall eines Tail-Aufrufs, bei dem die Prozedur selbst aufgerufen wird.)
Es enthält formale Definitionen für sechs verschiedene "Maschinen" zur Bewertung des Kernschemas, wobei jede Maschine das gleiche beobachtbare Verhalten aufweist, mit Ausnahme der asymptotischen Raumkomplexitätsklasse, in der sich jede befindet.
Nachdem Sie beispielsweise Definitionen für Computer mit jeweils 1. stapelbasierter Speicherverwaltung, 2. Speicherbereinigung, aber keinen Endaufrufen, 3. Speicherbereinigung und Endaufrufen angegeben haben, wird das Papier mit noch fortschrittlicheren Speicherverwaltungsstrategien fortgesetzt, z 4. "evlis tail recursion", bei der die Umgebung bei der Auswertung des letzten Unterausdrucksarguments in einem tail call nicht erhalten bleiben muss, 5. Reduzieren der Umgebung eines Abschlusses auf nur die freien Variablen dieses Abschlusses und 6. sogenannte "Safe-for-Space" -Semantik, wie sie von Appel und Shao definiert wurde .
Um zu beweisen, dass die Maschinen tatsächlich zu sechs verschiedenen Klassen der Raumkomplexität gehören, enthält das Papier für jedes verglichene Maschinenpaar konkrete Beispiele für Programme, die eine asymptotische Raumvergrößerung auf einer Maschine, jedoch nicht auf der anderen, aufdecken.
(Wenn ich jetzt meine Antwort durchlese, bin ich mir nicht sicher, ob es mir tatsächlich gelungen ist, die entscheidenden Punkte des Clinger-Papiers zu erfassen . Leider kann ich momentan nicht mehr Zeit für die Entwicklung dieser Antwort verwenden.)
quelle
Viele Leute haben hier bereits die Rekursion erklärt. Ich möchte einige Gedanken zu einigen Vorteilen anführen, die die Rekursion aus dem Buch „Parallelität in .NET, moderne Muster der gleichzeitigen und parallelen Programmierung“ von Riccardo Terrell bietet:
Hier sind auch einige interessante Anmerkungen aus demselben Buch über die Schwanzrekursion:
quelle