Comment savoir quand utiliser le pli à gauche et quand utiliser le pli à droite?

99

Je suis conscient que le pli à gauche produit des arbres penchés à gauche et le pli à droite produit des arbres penchés à droite, mais lorsque j'atteins un pli, je me retrouve parfois enlisé dans une pensée qui me donne des maux de tête en essayant de déterminer quel type de pli est approprié. Je finis généralement par dénouer tout le problème et par 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 quelques règles de base pour déterminer s'il faut se plier à gauche ou à droite?
  • Comment puis-je rapidement décider quel type de pli utiliser compte tenu du problème auquel je suis confronté?

Il existe un exemple dans Scala par exemple (PDF) d'utilisation d'un repli 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 pli droit 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 si courante dans la programmation (fonctionnelle), j'aimerais pouvoir prendre ce genre de décisions rapidement et en toute confiance. Alors ... des conseils?

Jeff
la source
1
ressemble à stackoverflow.com/questions/384797/…
Steven Huwig
1
Ce Q est plus général que celui, qui concernait spécifiquement Haskell. La paresse fait une grande différence dans la réponse à la question.
Chris Conway
Oh. Bizarre, je pensais avoir vu un tag Haskell sur cette question, mais je suppose que non ...
éphémère

Réponses:

105

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

Cet exemple se plie en utilisant la fonction d'accumulateur x

fold x [A, B, C, D]

donc égale

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 ( pli à 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 foldlplus répandus. Mais dans les autres cas, la notation infixe + parenthèses est assez utile.

Dario
la source
6
Eh bien, ce que vous avez décrit est en fait foldl1et foldr1en Haskell ( foldlet foldrprenez une valeur initiale externe), et les "inconvénients" de Haskell ne sont (:)pas appelés (::), mais sinon c'est correct. Vous pouvez ajouter que Haskell fournit en plus un foldl'/ foldl1'qui sont des variantes strictes de foldl/ foldl1, car l'arithmétique paresseuse n'est pas toujours souhaitable.
éphémère
Désolé, je pensais avoir vu un tag "Haskell" sur cette question, mais il n'y est pas. Mon commentaire n'a pas vraiment beaucoup de sens si ce n'est pas Haskell ...
éphémère
@ephemient Vous l'avez vu. C'est un "pseudocode de style haskell". :)
rire_man
La meilleure réponse que j'ai jamais vue concerne la différence entre les plis.
AleXoundOS
60

Olin Shivers les a différenciés en disant «foldl est l'itérateur de liste fondamental» et «foldr est l'opérateur de récursivité de liste fondamentale». Si vous regardez le fonctionnement de 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 le parcours vers le cas de base 4 et construire le résultat à partir de là.

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

Mais en réalité, cela sera probablement le plus évident à partir de l'associativité des opérateurs que vous utilisez. S'ils sont associatifs à gauche, utilisez foldl. S'ils sont associatifs à droite, utilisez foldr.

Steven Huwig
la source
29

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 exemple Scala dans votre question, je vais donner un exemple spécifique à Scala. Comme Tricks l'a déjà dit, il foldRightfaut conserver n-1les cadres de pile, où nest la longueur de votre liste et cela peut facilement conduire à un débordement de pile - et même la récursivité de queue ne pourrait pas vous sauver de cela.

A 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 a été fait dans la miseList en œuvre de .

Dans un langage strictement évalué comme Scala, a foldRightpeut facilement faire exploser la pile pour de grandes listes, alors que ce foldLeftn'est pas le cas.

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 d'or est donc - pour les opérateurs qui n'ont pas d'associativité spécifique, utilisez toujours foldLeft, au moins dans Scala. Sinon, suivez les autres conseils donnés dans les réponses;).

Flaviu Cipcigan
la source
13
C'était vrai auparavant, mais dans les versions actuelles de Scala, foldRight a été modifié pour appliquer foldLeft sur une copie inversée de la liste. Par exemple, dans la 2.10.3, github.com/scala/scala/blob/v2.10.3/src/library/scala/… . On dirait que ce changement a été effectué début 2013 - github.com/scala/scala/commit/… .
Dhruv Kapoor
4

Il convient également de noter (et je me rends compte que c'est un peu évident), 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 accumulée vers l'avant

foldr: a (1 + (2 + (3 + 4)))besoin d'un cadre de pile à ouvrir pour 1 + ?et 2 + ?avant le calcul 3 + 4, puis il doit revenir en arrière et faire le calcul pour chacun.

Je ne suis pas assez expert des langages fonctionnels ou des optimisations de compilateurs 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.


la source
1
Les cadres de pile supplémentaires feront certainement une différence pour les grandes listes. Si vos cadres de pile dépassent la taille du cache du processeur, vos échecs de cache affecteront les performances. À moins que la liste ne soit doublement liée, il est assez difficile de faire de foldr une fonction récursive de queue, vous devriez donc utiliser foldl à moins qu'il n'y ait une raison de ne pas le faire.
A. Levy
4
La nature paresseuse de Haskell brouille cette analyse. Si la fonction en cours de pliage n'est pas stricte dans le deuxième paramètre, alors foldrpeut très bien être plus efficace que foldl, et ne nécessitera pas de cadres de pile supplémentaires.
éphémère
2
Désolé, je pensais avoir vu un tag "Haskell" sur cette question, mais il n'y est pas. Mon commentaire n'a pas vraiment beaucoup de sens si ce n'est pas Haskell ...
éphémère