Foldl versus Foldr-Verhalten mit unendlichen Listen

124

Der Code für die Funktion myAny in dieser Frage verwendet foldr. Die Verarbeitung einer unendlichen Liste wird beendet, wenn das Prädikat erfüllt ist.

Ich habe es mit foldl umgeschrieben:

myAny :: (a -> Bool) -> [a] -> Bool
myAny p list = foldl step False list
   where
      step acc item = p item || acc

(Beachten Sie, dass die Argumente für die Schrittfunktion korrekt umgekehrt sind.)

Die Verarbeitung von unendlichen Listen wird jedoch nicht mehr gestoppt.

Ich habe versucht, die Ausführung der Funktion wie in Apocalisps Antwort zu verfolgen :

myAny even [1..]
foldl step False [1..]
step (foldl step False [2..]) 1
even 1 || (foldl step False [2..])
False  || (foldl step False [2..])
foldl step False [2..]
step (foldl step False [3..]) 2
even 2 || (foldl step False [3..])
True   || (foldl step False [3..])
True

Dies ist jedoch nicht das Verhalten der Funktion. Wie ist das falsch?

Titandecoy
quelle

Antworten:

231

Wie foldunterschiedlich die Unterschiede sind, scheint eine häufige Quelle der Verwirrung zu sein. Hier ein allgemeinerer Überblick:

Ziehen Sie in Betracht, eine Liste von n Werten [x1, x2, x3, x4 ... xn ]mit einer Funktion fund einem Startwert zu falten z.

foldl ist:

  • Linker Assoziativ :f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
  • Schwanz rekursiv : Es durchläuft die Liste und erzeugt anschließend den Wert
  • Faul : Nichts wird ausgewertet, bis das Ergebnis benötigt wird
  • Rückwärts : foldl (flip (:)) []Kehrt eine Liste um.

foldr ist:

  • Richtiger Assoziativ :f x1 (f x2 (f x3 (f x4 ... (f xn z) ... )))
  • Rekursiv in ein Argument : Jede Iteration gilt ffür den nächsten Wert und das Ergebnis des Faltens des Restes der Liste.
  • Faul : Nichts wird ausgewertet, bis das Ergebnis benötigt wird
  • Forwards : foldr (:) []gibt eine Liste unverändert.

Es ist ein etwas subtiler Punkt hier , dass Reisen Personen manchmal: Da foldlist rückwärts jede Anwendung fauf den hinzugefügt wird außerhalb des Ergebnisses; und weil es faul ist , wird nichts ausgewertet, bis das Ergebnis benötigt wird. Dies bedeutet , dass irgendein Teil des Ergebnisses, Haskell ersten iteriert durch die berechnen gesamte Liste Konstruktion eines Expressions von verschachtelten Funktionsanwendungen, wertet dann die äußerste Funktion, Bewertung ihrer Argumente wie erforderlich. Wenn fimmer das erste Argument verwendet wird, bedeutet dies, dass Haskell bis zum innersten Begriff zurückgreifen und dann jede Anwendung von rückwärts berechnen muss f.

Dies ist offensichtlich weit entfernt von der effizienten Schwanzrekursion, die die meisten funktionalen Programmierer kennen und lieben!

In der Tat kann es zu einem Stapelüberlauf kommen , obwohl dies foldltechnisch rekursiv ist, da der gesamte Ergebnisausdruck vor der Auswertung erstellt wird foldl!

Auf der anderen Seite überlegen foldr. Es ist auch faul, aber da es vorwärts läuft , wird jede Anwendung von fdem Inneren des Ergebnisses hinzugefügt . Um das Ergebnis zu berechnen, erstellt Haskell eine einzelne Funktionsanwendung, deren zweites Argument der Rest der gefalteten Liste ist. Wenn fdas zweite Argument - beispielsweise ein Datenkonstruktor - faul ist , ist das Ergebnis inkrementell faul , wobei jeder Schritt der Falte nur berechnet wird, wenn ein Teil des Ergebnisses ausgewertet wird, der es benötigt.

So können wir sehen, warum foldrmanchmal unendliche Listen foldlfunktionieren, wenn dies nicht der Fall ist: Ersteres kann eine unendliche Liste träge in eine andere träge unendliche Datenstruktur konvertieren, während letzteres die gesamte Liste untersuchen muss, um einen Teil des Ergebnisses zu generieren. Auf der anderen Seite foldrfunktioniert eine Funktion, die beide Argumente sofort benötigt, z. B. (+)funktioniert (oder funktioniert eher nicht), ähnlich wie das foldlErstellen eines großen Ausdrucks vor dem Auswerten.

Die beiden wichtigsten Punkte sind also:

  • foldr kann eine träge rekursive Datenstruktur in eine andere umwandeln.
  • Andernfalls stürzen faule Falten mit einem Stapelüberlauf auf großen oder unendlichen Listen ab.

Sie haben vielleicht bemerkt, dass es so klingt, als ob foldrSie alles foldlkönnen, und noch mehr. Das ist wahr! In der Tat ist Foldl fast nutzlos!

Aber was ist, wenn wir ein nicht faules Ergebnis erzielen wollen, indem wir eine große (aber nicht unendliche) Liste falten? Dafür wollen wir eine strikte Falte , die die Standardbibliotheken sorgfältig bereitstellen :

foldl' ist:

  • Linker Assoziativ :f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
  • Schwanz rekursiv : Es durchläuft die Liste und erzeugt anschließend den Wert
  • Streng : Jede Funktionsanwendung wird auf dem Weg bewertet
  • Rückwärts : foldl' (flip (:)) []Kehrt eine Liste um.

Weil foldl'es streng ist, das Ergebnis zu berechnen, wird Haskell bei jedem Schritt auswerten f , anstatt das linke Argument einen riesigen, nicht bewerteten Ausdruck ansammeln zu lassen. Dies gibt uns die übliche, effiziente Schwanzrekursion, die wir wollen! Mit anderen Worten:

  • foldl' kann große Listen effizient falten.
  • foldl' hängt in einer Endlosschleife (verursacht keinen Stapelüberlauf) in einer Endlosliste.

Das Haskell-Wiki hat auch eine Seite, auf der dies diskutiert wird.

CA McCann
quelle
6
Ich bin hierher gekommen, weil ich neugierig bin, warum foldres besser ist als foldlin Haskell , während das Gegenteil in Erlang (das ich vor Haskell gelernt habe ) zutrifft . Da Erlang ist nicht faul und Funktionen sind nicht curried , so foldlin Erlang verhält sich wie foldl'oben. Das ist eine großartige Antwort! Gute Arbeit und danke!
Siu Ching Pong -Asuka Kenji-
7
Dies ist meistens eine gute Erklärung, aber ich finde die Beschreibung foldlals "rückwärts" und foldrals "vorwärts" problematisch. Dies liegt zum Teil daran, flipdass (:)in der Abbildung angewendet wird, warum die Falte rückwärts ist. Die natürliche Reaktion ist: "Natürlich ist es rückwärts: Sie flipped Liste Verkettung!" Es ist auch seltsam zu sehen, dass "rückwärts" genannt wird, da dies foldlfür fdas erste Listenelement zuerst (innerste) in einer vollständigen Bewertung gilt. Es ist so, foldrdass "rückwärts läuft" und zuerst fauf das letzte Element angewendet wird.
Dave Abrahams
1
@ DaveAbrahams: Zwischen gerecht foldlund foldrund Ignorieren von Strenge und Optimierungen bedeutet zunächst "äußerste", nicht "innerste". Dies ist der Grund, warum foldrunendliche Listen verarbeitet werden können und foldlnicht - die rechte Falte gilt zuerst für fdas erste Listenelement und das (nicht bewertete) Ergebnis des Faltens des Schwanzes, während die linke Falte die gesamte Liste durchlaufen muss, um die äußerste Anwendung von zu bewerten f.
CA McCann
1
Ich frage mich nur, ob es einen Fall gibt, in dem Foldl Foldl vorgezogen wird. Glaubst du, es gibt einen?
Kazuoua
1
@kazuoua wo die Faulheit wesentlich ist, z last xs = foldl (\a z-> z) undefined xs.
Will Ness
28
myAny even [1..]
foldl step False [1..]
foldl step (step False 1) [2..]
foldl step (step (step False 1) 2) [3..]
foldl step (step (step (step False 1) 2) 3) [4..]

etc.

Intuitiv foldlist immer auf der "Außenseite" oder auf der "Linken", so dass es zuerst erweitert wird. Ad infinitum.

Artelius
quelle
10

Sie können in Haskells Dokumentation hier sehen, dass foldl rekursiv ist und niemals endet, wenn eine unendliche Liste übergeben wird, da es sich beim nächsten Parameter aufruft, bevor ein Wert zurückgegeben wird ...

Romain
quelle
0

Ich kenne Haskell nicht, aber in Schema fold-rightwird immer zuerst auf das letzte Element einer Liste "reagiert". Daher funktioniert es nicht für zyklische Listen (die mit einer unendlichen identisch sind).

Ich bin nicht sicher, ob fold-rightschwanzrekursiv geschrieben werden kann, aber für jede zyklische Liste sollten Sie einen Stapelüberlauf erhalten. fold-leftOTOH wird normalerweise mit Schwanzrekursion implementiert und bleibt nur in einer Endlosschleife stecken, wenn sie nicht vorzeitig beendet wird.

Leppie
quelle
3
In Haskell ist es anders wegen der Faulheit.
Lifu Huang