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?
Réponses:
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
donc égale
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
Ici, vous utilisez un pli gauche . Exemple (pseudocode de style haskell)
Si votre opérateur est associatif à droite ( pli à droite ), les parenthèses seraient définies comme ceci:
Exemple: contre-opérateur
En général, les opérateurs arithmétiques (la plupart des opérateurs) sont associatifs à gauche, donc
foldl
plus répandus. Mais dans les autres cas, la notation infixe + parenthèses est assez utile.la source
foldl1
etfoldr1
en Haskell (foldl
etfoldr
prenez 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 unfoldl'
/foldl1'
qui sont des variantes strictes defoldl
/foldl1
, car l'arithmétique paresseuse n'est pas toujours souhaitable.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:
vous pouvez voir l'accumulateur (comme dans une itération récursive de queue) en cours de construction. En revanche, foldr procède:
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.
la source
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
foldRight
faut conservern-1
les cadres de pile, oùn
est 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 à:tandis que
List(1,2,3).foldLeft(0)(_ + _)
se réduirait à:qui peut être calculé de manière itérative, comme cela a été fait dans la mise
List
en œuvre de .Dans un langage strictement évalué comme Scala, a
foldRight
peut facilement faire exploser la pile pour de grandes listes, alors que cefoldLeft
n'est pas le cas.Exemple:
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;).la source
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'avantfoldr: a
(1 + (2 + (3 + 4)))
besoin d'un cadre de pile à ouvrir pour1 + ?
et2 + ?
avant le calcul3 + 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
foldr
peut très bien être plus efficace quefoldl
, et ne nécessitera pas de cadres de pile supplémentaires.