Woher wissen Sie, wann Sie Fold-Left und wann Fold-Right verwenden müssen?


99

Ich bin mir bewusst, dass Linksfalte linksgerichtete Bäume und Rechtsfalte rechtsgerichtete Bäume hervorbringt, aber wenn ich nach einer Falte greife, bin ich manchmal in kopfschmerzauslösenden Gedanken versunken und versuche festzustellen, welche Art von Falte Ist angemessen. Normalerweise löse ich das gesamte Problem ab und gehe die Implementierung der Faltfunktion durch, die für mein Problem gilt.

Was ich also wissen möchte ist:

  • Welche Faustregeln gelten für die Entscheidung, ob nach links oder rechts gefaltet werden soll?
  • Wie kann ich angesichts des Problems, mit dem ich konfrontiert bin, schnell entscheiden, welche Art von Falte ich verwenden möchte?

In Scala by Example (PDF) gibt es ein Beispiel für die Verwendung einer Falte zum Schreiben einer Funktion namens Abflachen, die eine Liste von Elementlisten zu einer einzigen Liste zusammenfasst. In diesem Fall ist eine rechte Falte die richtige Wahl (angesichts der Art und Weise, wie die Listen verkettet sind), aber ich musste ein wenig darüber nachdenken, um zu dieser Schlussfolgerung zu gelangen.

Da das Falten eine so häufige Aktion in der (funktionalen) Programmierung ist, möchte ich in der Lage sein, solche Entscheidungen schnell und sicher zu treffen. Also ... irgendwelche Tipps?



1
Dieses Q ist allgemeiner als das, bei dem es speziell um Haskell ging. Faulheit macht einen großen Unterschied in der Antwort auf die Frage.
Chris Conway

Oh. Seltsam, irgendwie dachte ich, ich hätte ein Haskell-Tag auf dieser Frage gesehen, aber ich denke nicht ...
kurzlebig

Antworten:


105

Sie können eine Falte in eine Infix-Operator-Notation übertragen (dazwischen schreiben):

Dieses Beispiel falten mit der Akkumulatorfunktion x

fold x [A, B, C, D]

also gleich

A x B x C x D

Jetzt müssen Sie nur noch über die Assoziativität Ihres Operators nachdenken (indem Sie Klammern setzen!).

Wenn Sie einen linksassoziativen Operator haben, setzen Sie die Klammern wie folgt

((A x B) x C) x D

Hier verwenden Sie eine linke Falte . Beispiel (Pseudocode im Hashkell-Stil)

foldl (-) [1, 2, 3] == (1 - 2) - 3 == 1 - 2 - 3 // - is left-associative

Wenn Ihr Operator rechtsassoziativ ist ( rechte Falte ), werden die Klammern wie folgt gesetzt:

A x (B x (C x D))

Beispiel: Cons-Operator

foldr (:) [] [1, 2, 3] == 1 : (2 : (3 : [])) == 1 : 2 : 3 : [] == [1, 2, 3]

Im Allgemeinen sind arithmetische Operatoren (die meisten Operatoren) linksassoziativ und daher foldlweiter verbreitet. In den anderen Fällen ist die Infixnotation + Klammern sehr nützlich.


6
Nun, was Sie beschrieben haben, ist tatsächlich foldl1und foldr1in Haskell ( foldlund foldrnehmen Sie einen externen Anfangswert), und Haskells "Nachteile" werden (:)nicht genannt (::), aber ansonsten ist dies korrekt. Vielleicht möchten Sie hinzufügen, dass Haskell zusätzlich ein foldl'/ bereitstellt, foldl1'das strenge Varianten von foldl/ sind foldl1, da faule Arithmetik nicht immer wünschenswert ist.
Ephemient

Entschuldigung, ich dachte, ich hätte ein "Haskell" -Tag zu dieser Frage gesehen, aber es ist nicht da. Mein Kommentar macht nicht wirklich viel Sinn, wenn es nicht Haskell ist ...
kurzlebig

@ Ephemient Du hast es gesehen. Es ist "Pseudocode im Haskell-Stil". :)
Laughing_man

Die beste Antwort, die ich je gesehen habe, hängt mit dem Unterschied zwischen den Falten zusammen.
AleXoundOS

60

Olin Shivers differenzierte sie mit den Worten "foldl ist der grundlegende Listeniterator" und "foldr ist der grundlegende Listenrekursionsoperator". Wenn Sie sich ansehen, wie Foldl funktioniert:

((1 + 2) + 3) + 4

Sie können sehen, wie der Akkumulator (wie in einer rekursiven Iteration) erstellt wird. Im Gegensatz dazu fährt foldr fort:

1 + (2 + (3 + 4))

Hier können Sie die Durchquerung des Basisfalls 4 sehen und von dort aus das Ergebnis aufbauen.

Ich setze also eine Faustregel auf: Wenn es wie eine Listeniteration aussieht, die einfach in schwanzrekursiver Form zu schreiben wäre, ist foldl der richtige Weg.

Aber dies wird wahrscheinlich am deutlichsten an der Assoziativität der von Ihnen verwendeten Operatoren ersichtlich. Wenn sie linksassoziativ sind, verwenden Sie foldl. Wenn sie rechtsassoziativ sind, verwenden Sie foldr.


29

Andere Poster haben gute Antworten gegeben und ich werde nicht wiederholen, was sie bereits gesagt haben. Da Sie in Ihrer Frage ein Scala-Beispiel angegeben haben, gebe ich ein Scala-spezifisches Beispiel. Wie Tricks bereits sagte, muss ein foldRightStapelrahmen beibehalten n-1werden, wobei ndie Länge Ihrer Liste und dies leicht zu einem Stapelüberlauf führen kann - und nicht einmal die Schwanzrekursion könnte Sie davor bewahren.

A List(1,2,3).foldRight(0)(_ + _)würde sich reduzieren auf:

1 + List(2,3).foldRight(0)(_ + _)        // first stack frame
    2 + List(3).foldRight(0)(_ + _)      // second stack frame
        3 + 0                            // third stack frame 
// (I don't remember if the JVM allocates space 
// on the stack for the third frame as well)

während List(1,2,3).foldLeft(0)(_ + _)würde sich reduzieren auf:

(((0 + 1) + 2) + 3)

die iterativ berechnet werden kann, wie bei der Implementierung vonList .

In einer streng bewerteten Sprache wie Scala foldRightkann a den Stapel für große Listen leicht in die Luft jagen, während a dies foldLeftnicht tut.

Beispiel:

scala> List.range(1, 10000).foldLeft(0)(_ + _)
res1: Int = 49995000

scala> List.range(1, 10000).foldRight(0)(_ + _)
java.lang.StackOverflowError
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRig...

Meine Faustregel lautet daher: Für Operatoren, die keine bestimmte Assoziativität haben, verwenden Sie immer foldLeft, zumindest in Scala. Andernfalls gehen Sie mit anderen Ratschlägen in den Antworten;).


13
Dies war früher der Fall, aber in aktuellen Versionen von Scala wurde foldRight geändert, um foldLeft auf eine umgekehrte Kopie der Liste anzuwenden. Zum Beispiel in 2.10.3, github.com/scala/scala/blob/v2.10.3/src/library/scala/… . Diese Änderung wurde anscheinend Anfang 2013 vorgenommen - github.com/scala/scala/commit/… .
Dhruv Kapoor

4

Es ist auch erwähnenswert (und mir ist klar, dass dies ein wenig offensichtlich ist), dass im Fall eines kommutativen Operators die beiden ziemlich gleichwertig sind. In dieser Situation könnte ein Foldl die bessere Wahl sein:

foldl: (((1 + 2) + 3) + 4)kann jede Operation berechnen und den akkumulierten Wert übertragen

foldr: Muss (1 + (2 + (3 + 4)))ein Stapelrahmen für 1 + ?und 2 + ?vor der Berechnung geöffnet werden 3 + 4, dann muss er zurückgehen und die Berechnung für jeden durchführen.

Ich bin nicht genug Experte für funktionale Sprachen oder Compiler-Optimierungen, um zu sagen, ob dies tatsächlich einen Unterschied macht, aber es scheint auf jeden Fall sauberer, ein Foldl mit kommutativen Operatoren zu verwenden.


1
Die zusätzlichen Stapelrahmen machen definitiv einen Unterschied für große Listen. Wenn Ihre Stack-Frames die Größe des Prozessor-Cache überschreiten, beeinträchtigen Ihre Cache-Fehler die Leistung. Wenn die Liste nicht doppelt verknüpft ist, ist es schwierig, foldr zu einer schwanzrekursiven Funktion zu machen. Daher sollten Sie foldl verwenden, es sei denn, es gibt einen Grund, dies nicht zu tun.
A. Levy

4
Die Faulheit von Haskell verwirrt diese Analyse. Wenn die zu faltende Funktion im zweiten Parameter nicht streng ist, foldrkann sie sehr wohl effizienter sein als foldlund erfordert keine zusätzlichen Stapelrahmen.
Ephemient

2
Entschuldigung, ich dachte, ich hätte ein "Haskell" -Tag zu dieser Frage gesehen, aber es ist nicht da. Mein Kommentar macht nicht wirklich viel Sinn, wenn es nicht Haskell ist ...
kurzlebig
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.