web-dev-qa-db-fra.com

Comment savez-vous quand utiliser le rabat gauche et quand utiliser le rabat droit?

Je suis conscient que le repli vers la gauche produit des arbres inclinés à gauche et le repli vers la droite produit des arbres inclinés à droite, mais quand j'atteins un pli, je me retrouve parfois enlisé dans une pensée provoquant des maux de tête essayant de déterminer quel type de pli est approprié. Je finis généralement par dérouler l'ensemble du problème et passer à travers la mise en œuvre de la fonction de pliage telle qu'elle s'applique à mon problème.

Donc ce que je veux savoir c'est:

  • Quelles sont les règles de base pour déterminer s'il faut plier à gauche ou à droite?
  • Comment puis-je décider rapidement quel type de pli utiliser en fonction du problème auquel je suis confronté?

Il y a un exemple dans Scala par exemple (PDF) d'utilisation d'un pli pour écrire une fonction appelée flatten qui concatène une liste de listes d'éléments en une seule liste. Dans ce cas, un bon pli est le bon choix (étant donné la façon dont les listes sont concaténées), mais j'ai dû y réfléchir un peu pour arriver à cette conclusion.

Étant donné que le pliage est une action courante dans la programmation (fonctionnelle), j'aimerais pouvoir prendre ce type de décisions rapidement et en toute confiance. Alors ... des conseils?

94
Jeff

Vous pouvez transférer un pli dans une notation d'opérateur infixe (en écrivant entre les deux):

Cet exemple se replie en utilisant la fonction accumulateur x

fold x [A, B, C, D]

est donc égal

A x B x C x D

Il ne vous reste plus qu'à raisonner sur l'associativité de votre opérateur (en mettant des parenthèses!).

Si vous avez un opérateur associatif à gauche, vous définissez les parenthèses comme ceci

((A x B) x C) x D

Ici, vous utilisez un pli gauche. Exemple (pseudocode de style haskell)

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

Si votre opérateur est associatif à droite (repli à droite), les parenthèses seraient définies comme ceci:

A x (B x (C x D))

Exemple: Contre-opérateur

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

En général, les opérateurs arithmétiques (la plupart des opérateurs) sont associatifs à gauche, donc foldl est plus répandu. Mais dans les autres cas, la notation infixe + parenthèses est très utile.

99
Dario

Olin Shivers les a différenciés en disant que "foldl est l'itérateur de liste fondamental" et "foldr est l'opérateur de récursion de liste fondamental". Si vous regardez comment fonctionne foldl:

((1 + 2) + 3) + 4

vous pouvez voir l'accumulateur (comme dans une itération récursive de queue) en cours de construction. En revanche, foldr procède:

1 + (2 + (3 + 4))

où vous pouvez voir la traversée vers le cas de base 4 et construire le résultat à partir de là.

Je pose donc une règle de base: si cela ressemble à une itération de liste, une qui serait simple à écrire sous forme récursive de queue, foldl est la voie à suivre.

Mais ce sera probablement le plus évident d'après l'associativité des opérateurs que vous utilisez. S'ils sont associatifs à gauche, utilisez foldl. S'ils sont associatifs à droite, utilisez foldr.

60
Steven Huwig

D'autres affiches ont donné de bonnes réponses et je ne répéterai pas ce qu'elles ont déjà dit. Comme vous avez donné un Scala exemple dans votre question, je vais donner un Scala exemple spécifique. Comme Astuces déjà dit, un foldRight doit conserver les cadres de pile n-1, où n est la longueur de votre liste et cela peut facilement entraîner un débordement de pile - et même une récursivité de la queue ne pourrait pas vous sauver de cela.

Une List(1,2,3).foldRight(0)(_ + _) se réduirait à:

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)

tandis que List(1,2,3).foldLeft(0)(_ + _) se réduirait à:

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

qui peut être calculé de manière itérative, comme cela est fait dans le implémentation de List .

Dans un langage strictement évalué comme Scala, un foldRight peut facilement faire exploser la pile de grandes listes, contrairement à un foldLeft.

Exemple:

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...

Ma règle générale est donc - pour les opérateurs qui n'ont pas d'associativité spécifique, utilisez toujours foldLeft, au moins dans Scala. Sinon, allez avec d'autres conseils donnés dans les réponses;).

28
Flaviu Cipcigan

Il convient également de noter (et je me rends compte que cela indique un peu l'évidence), dans le cas d'un opérateur commutatif, les deux sont à peu près équivalents. Dans cette situation, un foldl pourrait être le meilleur choix:

foldl: (((1 + 2) + 3) + 4) peut calculer chaque opération et reporter la valeur cumulée

foldr: (1 + (2 + (3 + 4))) nécessite l'ouverture d'un cadre de pile pour 1 + ? et 2 + ? avant de calculer 3 + 4, il doit ensuite revenir en arrière et faire le calcul pour chacun.

Je ne suis pas assez expert en langages fonctionnels ou en optimisations de compilateur pour dire si cela fera réellement une différence, mais il semble certainement plus propre d'utiliser un foldl avec des opérateurs commutatifs.

4
Tricks