it-swarm.com.de

Sind funktionale Sprachen besser rekursiv?

TL; DR: Behandeln funktionale Sprachen die Rekursion besser als nicht funktionale?

Ich lese gerade Code Complete 2. Irgendwann im Buch warnt uns der Autor vor einer Rekursion. Er sagt, es sollte nach Möglichkeit vermieden werden und dass Funktionen mit Rekursion im Allgemeinen weniger effektiv sind als eine Lösung mit Schleifen. Als Beispiel schrieb der Autor eine Java -Funktion unter Verwendung von Rekursion, um die Fakultät einer solchen Zahl zu berechnen (sie ist möglicherweise nicht genau dieselbe, da ich das Buch momentan nicht bei mir habe ):

public int factorial(int x) {
    if (x <= 0)
        return 1;
    else
        return x * factorial(x - 1);
}

Dies wird als schlechte Lösung dargestellt. In funktionalen Sprachen ist die Verwendung von Rekursion jedoch häufig die bevorzugte Methode. Hier ist zum Beispiel die Fakultätsfunktion in Haskell unter Verwendung der Rekursion:

factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n - 1)

Und wird allgemein als gute Lösung akzeptiert. Wie ich gesehen habe, verwendet Haskell sehr oft Rekursion, und ich habe nirgendwo gesehen, dass sie verpönt ist.

Meine Frage lautet also im Grunde:

  • Behandeln funktionale Sprachen die Rekursion besser als nicht funktionale?

EDIT: Mir ist bewusst, dass die Beispiele, die ich verwendet habe, nicht die besten sind, um meine Frage zu veranschaulichen. Ich wollte nur darauf hinweisen, dass Haskell (und funktionale Sprachen im Allgemeinen) Rekursion viel häufiger verwendet als nicht funktionale Sprachen.

42
marco-fiset

Ja, sie tun es, aber nicht nur, weil sie können, sondern weil sie müssen.

Das Schlüsselkonzept hier ist Reinheit: Eine reine Funktion ist eine Funktion ohne Nebenwirkungen und ohne Zustand. Funktionale Programmiersprachen umfassen im Allgemeinen Reinheit aus vielen Gründen, z. B. zum Nachdenken über Code und zum Vermeiden nicht offensichtlicher Abhängigkeiten. Einige Sprachen, insbesondere Haskell, gehen sogar so weit, nur reinen Code zuzulassen; Alle Nebenwirkungen, die ein Programm haben kann (z. B. das Ausführen von E/A), werden in eine nicht reine Laufzeit verschoben, wodurch die Sprache selbst rein bleibt.

Wenn Sie keine Nebenwirkungen haben, können Sie keine Schleifenzähler haben (da ein Schleifenzähler einen veränderlichen Zustand darstellen würde und das Ändern eines solchen Zustands ein Nebeneffekt wäre). Das iterativste, was eine reine Funktionssprache erreichen kann, ist daher, über ein - zu iterieren list (diese Operation wird normalerweise foreach oder map genannt). Die Rekursion ist jedoch eine natürliche Übereinstimmung mit der reinen funktionalen Programmierung. Für die Rekursion ist kein Status erforderlich, mit Ausnahme der (schreibgeschützten) Funktionsargumente und eines (schreibgeschützten) Rückgabewerts.

Das Fehlen von Nebenwirkungen bedeutet jedoch auch, dass die Rekursion effizienter implementiert werden kann und der Compiler sie aggressiver optimieren kann. Ich habe selbst keinen solchen Compiler eingehend untersucht, aber soweit ich das beurteilen kann, führen die Compiler der meisten funktionalen Programmiersprachen eine Tail-Call-Optimierung durch, und einige kompilieren möglicherweise sogar bestimmte Arten von rekursiven Konstrukten in Schleifen hinter den Kulissen.

37
tdammers

Sie vergleichen Rekursion mit Iteration. Ohne Tail-Call-Elimination ist die Iteration in der Tat effizienter, da es keinen zusätzlichen Funktionsaufruf gibt. Außerdem kann die Iteration für immer fortgesetzt werden, während bei zu vielen Funktionsaufrufen möglicherweise nicht mehr genügend Speicherplatz vorhanden ist.

Für die Iteration muss jedoch ein Zähler geändert werden. Das heißt, es muss eine veränderbare Variable geben, die in einer rein funktionalen Umgebung verboten ist. Daher sind funktionale Sprachen speziell für den Betrieb ohne Iteration konzipiert, daher die optimierten Funktionsaufrufe.

Aber nichts davon spricht dafür, warum Ihr Codebeispiel so elegant ist. Ihr Beispiel zeigt eine andere Eigenschaft, nämlich Mustervergleich . Aus diesem Grund hat das Haskell-Beispiel keine expliziten Bedingungen. Mit anderen Worten, es ist nicht die optimierte Rekursion, die Ihren Code klein macht. Es ist der Mustervergleich.

18
chrisaycock

Technisch nein, aber praktisch ja.

Rekursion ist weitaus häufiger, wenn Sie das Problem funktional angehen. Daher enthalten Sprachen, die für die Verwendung eines funktionalen Ansatzes entwickelt wurden, häufig Funktionen, die die Rekursion einfacher/besser/weniger problematisch machen. Auf den ersten Blick gibt es drei häufige:

  1. Tail Call Optimization. Wie in anderen Postern erwähnt, erfordern funktionale Sprachen häufig TCO.

  2. Lazy Evaluation. Haskell (und einige andere Sprachen) werden träge bewertet. Dies verzögert die eigentliche "Arbeit" einer Methode, bis sie benötigt wird. Dies führt tendenziell zu rekursiveren Datenstrukturen und im weiteren Sinne zu rekursiven Methoden, um diese zu bearbeiten.

  3. Unveränderlichkeit. Die meisten Dinge, mit denen Sie in funktionalen Programmiersprachen arbeiten, sind unveränderlich. Dies erleichtert die Rekursion, da Sie sich nicht mit dem Zustand von Objekten im Laufe der Zeit befassen müssen. Sie können beispielsweise keinen Wert unter Ihnen ändern lassen. Viele Sprachen sind auch so konzipiert, dass sie reine Funktionen erkennen. Da reine Funktionen keine Nebenwirkungen haben, hat der Compiler viel mehr Freiheit hinsichtlich der Reihenfolge, in der die Funktionen ausgeführt werden, und anderer Optimierungen.

Keines dieser Dinge ist wirklich spezifisch für funktionale Sprachen im Vergleich zu anderen, daher sind sie nicht einfach besser, weil sie funktional sind. Da sie jedoch funktional sind, tendieren die getroffenen Entwurfsentscheidungen zu diesen Funktionen, da sie bei der funktionalen Programmierung nützlicher (und ihre Nachteile weniger problematisch) sind.

5
Telastyn

Der einzige technische Grund, den ich kenne, ist, dass einige funktionale Sprachen (und einige zwingende Sprachen, wenn ich mich recht erinnere) so genannte Tail Call Optimization haben, mit denen eine rekursive Methode die Größe des Stapels nicht erhöhen kann Jeder rekursive Aufruf (dh der rekursive Aufruf ersetzt mehr oder weniger den aktuellen Aufruf auf dem Stapel).

Beachten Sie, dass diese Optimierung bei rekursiven Aufrufen nicht funktioniert, sondern nur bei rekursiven Tail-Call-Methoden (dh Methoden, die den Status am nicht beibehalten) Zeit des rekursiven Aufrufs)

1
Steven Evers

Haskell und andere funktionale Sprachen verwenden im Allgemeinen eine verzögerte Auswertung. Mit dieser Funktion können Sie nicht endende rekursive Funktionen schreiben.

Wenn Sie eine rekursive Funktion schreiben, ohne einen Basisfall zu definieren, in dem die Rekursion endet, werden diese Funktion und der Stapelüberlauf unendlich aufgerufen.

Haskell unterstützt auch rekursive Funktionsaufrufoptimierungen. In Java würde sich jeder Funktionsaufruf stapeln und Overhead verursachen.

Ja, funktionale Sprachen können mit Rekursion besser umgehen als andere.

1
Mert Akcakaya

Sie sollten sich Garbage Collection ist schnell, aber ein Stapel ist schneller ansehen, ein Artikel über die Verwendung dessen, was C-Programmierer als "Heap" für die Stapelrahmen in kompiliertem C betrachten würden Ich glaube, der Autor hat mit Gcc daran herumgebastelt. Es ist keine eindeutige Antwort, aber das könnte Ihnen helfen, einige der Probleme mit der Rekursion zu verstehen.

Die Programmiersprache Alef , die früher mit Plan 9 von Bell Labs geliefert wurde, hatte eine "Werden" -Anweisung (siehe Abschnitt 6.6.4 von diese Referenz ). Es ist eine Art explizite Tail-Call-Rekursionsoptimierung. Das "aber es verbraucht Call Stack!" Argumente gegen die Rekursion könnten möglicherweise beseitigt werden.

1
Bruce Ediger

TL; DR: Ja, das tun sie
Rekursion ist ein Schlüsselwerkzeug in der funktionalen Programmierung, und daher wurde viel Arbeit in die Optimierung dieser Aufrufe gesteckt. Zum Beispiel erfordert R5RS (in der Spezifikation!), Dass alle Implementierungen ungebundene Tail-Rekursionsaufrufe verarbeiten, ohne dass sich der Programmierer um einen Stapelüberlauf sorgen muss. Zum Vergleich führt der C-Compiler standardmäßig nicht einmal eine offensichtliche Tail-Call-Optimierung durch (versuchen Sie eine rekursive Umkehrung einer verknüpften Liste), und nach einigen Aufrufen wird das Programm beendet (der Compiler optimiert jedoch, wenn Sie - verwenden. O2).

Natürlich hat der Compiler in Programmen, die schrecklich geschrieben sind, wie dem berühmten exponentiellen Beispiel fib, kaum oder gar keine Optionen, um seine 'Magie' auszuführen. Daher sollte darauf geachtet werden, die Compiler-Bemühungen bei der Optimierung nicht zu behindern.

EDIT: Mit dem Fib-Beispiel meine ich Folgendes:

(define (fib n)
 (if (< n 3) 1 
  (+ (fib (- n 1)) (fib (- n 2)))
 )
)
0
K.Steff

Funktionale Sprachen sind bei zwei sehr spezifischen Arten der Rekursion besser: Schwanzrekursion und unendliche Rekursion. Sie sind bei anderen Arten der Rekursion genauso schlecht wie andere Sprachen, wie Ihr Beispiel factorial.

Das heißt nicht, dass es in beiden Paradigmen keine Algorithmen gibt, die mit regelmäßiger Rekursion gut funktionieren. Zum Beispiel ist alles, was ohnehin eine stapelartige Datenstruktur erfordert, wie eine Tiefensuche, mit Rekursion am einfachsten zu implementieren.

Rekursion tritt häufiger bei der funktionalen Programmierung auf, wird aber auch häufig verwendet, insbesondere von Anfängern oder in Tutorials für Anfänger, möglicherweise weil die meisten Anfänger der funktionalen Programmierung die Rekursion zuvor in der imperativen Programmierung verwendet haben. Es gibt andere funktionale Programmierkonstrukte wie Listenverständnisse, Funktionen höherer Ordnung und andere Operationen für Sammlungen, die normalerweise konzeptionell, stilistisch, prägnant, effizient und optimierungsfähig sind.

Zum Beispiel Delnans Vorschlag von factorial n = product [1..n] ist nicht nur prägnanter und leichter zu lesen, sondern auch hochgradig parallelisierbar. Gleiches gilt für die Verwendung von fold oder reduce, wenn in Ihrer Sprache nicht bereits ein product integriert ist. Rekursion ist die Lösung des letzten Auswegs für dieses Problem. Der Hauptgrund, warum Sie sehen, dass es in Tutorials rekursiv gelöst wird, ist ein Ausgangspunkt, bevor Sie zu besseren Lösungen gelangen, und nicht ein Beispiel für eine bewährte Methode.

0
Karl Bielefeldt