Gibt es Probleme, die nicht geschrieben werden kann mit tail-Rekursion?
Tail-Rekursion ist ein wichtiger performance-Optimierung stragegy in funktionalen Sprachen, denn es erlaubt rekursive Aufrufe verbrauchen Konstante stack (anstatt O(n)).
Gibt es irgendwelche Probleme, die einfach nicht geschrieben werden kann in eine tail-rekursive Stil, oder ist es immer möglich, zu konvertieren, eine naiv-rekursive Funktion in eine tail-rekursive einer?
Wenn das so ist, eines Tages möglicherweise funktional-Compiler und-Interpreter intelligent genug, um die Konvertierung automatisch?
- Ich bin nicht einverstanden, dass tail recursion ist ein performance-Optimierung. Es hat zu tun mit der Richtigkeit: Programmierung in einer Sprache ohne dies verhindert, dass Sie von der Behandlung der unbegrenzten Wiederholung mit Rekursion, in der Erwägung, dass in Sprachen mit dieser Funktion, Sie sind frei, verwenden Sie die Rekursion (mit gewissen Einschränkungen) für unbegrenzte Wiederholung. Dies ist ein wichtiger Punkt: Die (Garantie -) Vorhandensein von tail-Rekursion ändert sich die Semantik der Sprache.
- Wie ändert sich die Semantik? Die gleiche abstrakte Berechnung ist beschrieben in jedem Fall. Die zeitliche und räumliche Komplexität der resultierende code ist wohl eine Implementierung detail, wenn auch ein wichtiger, wenn Sie erwarten, um wirklich das Programm ausführen.
- Ich sollte vielleicht qualifiziert haben, dass es Veränderungen der Semantik, wenn Sie eine statische Stapel, die ich verstehen ist Normalfall, z.B. mit Java können Sie übergeben Sie einen Parameter beim Start der jvm, wie groß der stack sein soll, aber der Stapel kann nicht wachsen und wachsen, bis die Maschine die Speicher aufgebraucht ist. Sie sprechen von der abstrakten Berechnung, und Sie sind in einem gewissen Sinne richtig, aber dies ist ein klassisches Beispiel einer "leaky abstraction": Das zugrunde liegende Implementierung übt sich, und ohne eine Garantie für tail-calls, die der Programmierer kann sich auf Sie verlassen.
Du musst angemeldet sein, um einen Kommentar abzugeben.
Ja, eigentlich ist Sie kann nehmen einige code und konvertieren jedem Aufruf und jeder Rückkehr in eine tail-call. Was Sie am Ende mit ist sogenannte continuation-passing-style, oder CPS.
Zum Beispiel, hier ist eine Funktion, die zwei rekursive Aufrufe:
Und hier ist, wie es Aussehen würde, wenn Sie konvertiert diese Funktion auf eine continuation-passing-style:
Dem zusätzlichen argument,
ctn
ist eine Prozedur, diecount-tree-cps
tail-calls anstatt. (sdcvvc Antwort sagt, dass Sie können nicht alles tun, was in O(1) Speicherplatz, und das ist richtig; dabei wird jede Fortsetzung ist ein closure, das braucht etwas Speicher).Wusste ich nicht, transformieren Sie die Aufrufe
car
odercdr
oder+
in tail-calls. Das könnte auch getan werden, aber ich nehme an, diejenigen, Blatt aufruft, wäre tatsächlich inlined.Nun zum spaßigen Teil. Chicken Scheme tatsächlich diese Umwandlung auf alle code kompiliert. Verfahren zusammengestellt von Huhn nie wieder. Es ist ein klassisches Papier, die erklärt, warum Hühner-Schema bedeutet dies, geschrieben im Jahre 1994 vor dem Huhn wurde umgesetzt: NACHTEILE: sollte nicht die Nachteile, die seine Argumente, Teil II: Cheney auf dem M. T. A.
Überraschend genug, continuation-passing-style ist ziemlich Häufig in JavaScript. Sie können es verwenden,zu lange andauernde Berechnung, die Vermeidung der browser "langsames Skript" popup. Und es ist attraktiv für asynchrone APIs.
jQuery.get
(ein einfacher wrapper um XMLHttpRequest) ist eindeutig in continuation-passing-style; das Letzte argument ist eine Funktion.count-tree-cps
enthalten geschachtelte Aufrufe zu sich selbst, anstattcount-tree
?Es ist wahr, aber nicht hilfreich, zu beachten, dass jede Sammlung von wechselseitig rekursiven Funktionen, die sich in eine tail-rekursive Funktion. Diese Beobachtung ist auf einer Stufe mit den alten Kastanien her den 1960er Jahren, die control-flow-Konstrukte beseitigt werden könnte, da jedes Programm geschrieben werden kann, wie eine Schleife mit einer case-Anweisung verschachtelte innerhalb.
Was nützlich zu wissen ist, dass viele Funktionen, die nicht offensichtlich tail-rekursive umgewandelt werden können, um tail-rekursive form durch die Zugabe von akkumulierende Parameter. (Eine extreme version dieser transformation ist die transformation auf eine continuation-passing-style (CPS), aber die meisten Programmierer finden, die die Ausgabe der CPS-Transformation schwer zu Lesen.)
Hier ist ein Beispiel für eine Funktion, die "rekursive" (eigentlich ist es nur Durchlaufen), aber nicht tail-rekursiv:
In diesem Fall mehrfach passiert nach des rekursiven Aufrufs.
Wir erstellen eine version, die ist tail-rekursiv, indem Sie das Produkt in eine akkumulierende parameter:
Die innere Funktion
f
ist tail-rekursive und kompiliert in einer engen Schleife.Finde ich folgende Unterscheidungen hilfreich:
In einem iterativen oder rekursiven Programm, Sie lösen ein problem der Größe
n
durcherste Lösung eine subproblem der Größe
n-1
. Computing-die Fakultät-Funktionin diese Kategorie fällt, und es kann getan werden, entweder iterativ oder
rekursiv. (Diese Idee verallgemeinert, z.B., um die Fibonacci-Funktion, wo
Sie brauchen beide
n-1
undn-2
zu lösenn
.)In einem rekursiven Programm, das Sie lösen ein problem der Größe
n
durch die erste Lösung zweiTeilprobleme der Größe
n/2
. Oder, mehr im Allgemeinen, Sie lösen ein problem der Größen
durch die erste Lösung ist, ein subproblem von Größe
k
und einer Größen-k
, wo1 < k <
. Quicksort und mergesort sind zwei Beispiele für diese Art von problem, dien
kann leicht programmiert werden, die rekursiv, aber nicht so einfach zu Programmieren
iterativ oder mit nur tail-Rekursion. (Sie müssen im wesentlichen simulieren Sie die Rekursion in eine explizite
stack.)
In der dynamischen Programmierung lösen Sie ein problem der Größe
n
durch die erste Lösung alleTeilproblemen in allen Größen
k
, wok<n
. Das finden der kürzesten route von einemPunkt zu einem anderen auf die Londoner U-Bahn ist ein Beispiel für diese Art von
problem. (Die Londoner U-Bahn ist ein mehrfach zusammenhš angender graph, und Sie
lösen Sie das problem, indem Sie zuerst das Auffinden aller Punkte, für die der kürzeste Pfad
ist 1 stop, dann für die der kürzeste Pfad ist 2 Haltestellen, etc, etc.)
Nur die erste Art von Programm hat eine einfach transformation in tail-rekursive form.
Jeder rekursive Algorithmus kann umgeschrieben werden als ein iterativer Algorithmus (vielleicht die einen Stapel oder Liste) und iterative algorithmen kann immer geschrieben werden als tail-rekursive algorithmen, so dass ich denke, es ist wahr, dass jede rekursive Lösung kann irgendwie konvertiert werden, um eine tail-rekursive Lösung.
(In den Kommentaren, Pascal Cuoq weist darauf hin, dass jeder Algorithmus umgewandelt werden können, um continuation-passing-style.)
Beachten Sie, dass, nur weil etwas ist tail-rekursiv ist, bedeutet nicht, dass die Speichernutzung ist konstant. Es bedeutet nur, dass der call-return-Stapel wächst nicht.
Können Sie nicht alles tun, was in O(1) Weltraum (space-Hierarchie theorem). Wenn Sie darauf bestehen, tail recursion, dann speichern Sie den call-stack als eines der Argumente. Offensichtlich dadurch ändert sich nichts; irgendwo intern, es ist ein call-stack, du bist einfach so dass es explizit sichtbar.
Solche Umwandlung wird sich nicht verringern, räumliche Komplexität.
Als Pascal Cuoq, kommentiert ein anderer Weg ist CPS; alle Anrufe sind tail-rekursive dann.
Ich glaube nicht, dass so etwas wie tak umgesetzt werden könnte, nur mithilfe von tail calls. (nicht erlaubt Fortsetzungen)