Premièrement, Real World Haskell, que je lis, dit de ne jamais utiliser foldl
et d'utiliser à la place foldl'
. Je lui fais donc confiance.
Mais je ne sais pas quand utiliser foldr
vs foldl'
. Bien que je puisse voir la structure de leur fonctionnement différemment présentée devant moi, je suis trop stupide pour comprendre quand "ce qui est mieux". Je suppose qu'il me semble que cela ne devrait pas vraiment avoir d'importance, car ils produisent tous les deux la même réponse (n'est-ce pas?). En fait, mon expérience précédente avec cette construction provient de Ruby inject
et de Clojure reduce
, qui ne semblent pas avoir de versions "gauche" et "droite". (Question secondaire: quelle version utilisent-ils?)
Tout aperçu qui peut aider un type intelligent comme moi serait très apprécié!
La récursivité pour foldr f x ys
où ys = [y1,y2,...,yk]
ressemble à
f y1 (f y2 (... (f yk x) ...))
alors que la récursivité pour foldl f x ys
ressemble à
f (... (f (f x y1) y2) ...) yk
Une différence importante ici est que si le résultat de f x y
peut être calculé en utilisant uniquement la valeur de x
, alors foldr
n'a pas besoin d'examiner la liste entière. Par exemple
foldr (&&) False (repeat False)
renvoie False
alors que
foldl (&&) False (repeat False)
ne se termine jamais. (Remarque: repeat False
crée une liste infinie où chaque élément est False
.)
D'autre part, foldl'
est récursif et strict. Si vous savez que vous devrez parcourir toute la liste quoi qu'il arrive (par exemple, en additionnant les nombres dans une liste), alors foldl'
est plus efficace en termes d'espace (et probablement de temps) que foldr
.
Leur sémantique étant différente, vous ne pouvez pas simplement échanger foldl
et foldr
. L'un plie les éléments vers le haut à gauche, l'autre à droite. De cette façon, l'opérateur est appliqué dans un ordre différent. Cela est important pour toutes les opérations non associatives, telles que la soustraction.
Haskell.org a une intéressante article sur le sujet.
En bref, foldr
est meilleur lorsque la fonction accumulateur est paresseuse sur son deuxième argument. En savoir plus sur le wiki de Haskell Stack Overflow (jeu de mots voulu).
La raison pour laquelle foldl'
Est préféré à foldl
pour 99% de toutes les utilisations est qu'il peut s'exécuter dans un espace constant pour la plupart des utilisations.
Prenez la fonction sum = foldl['] (+) 0
. Lorsque foldl'
Est utilisé, la somme est immédiatement calculée, donc l'application de sum
à une liste infinie s'exécutera pour toujours, et très probablement dans un espace constant (si vous utilisez des choses comme Int
s, Double
s, Float
s. Integer
s utilisera plus qu'un espace constant si le nombre devient supérieur à maxBound :: Int
).
Avec foldl
, un thunk est construit (comme une recette sur la façon d'obtenir la réponse, qui peut être évaluée plus tard, plutôt que de stocker la réponse). Ces thunks peuvent prendre beaucoup d'espace, et dans ce cas, il vaut bien mieux évaluer l'expression que de stocker le thunk (conduisant à un débordement de pile ... et vous menant à ... oh tant pis)
J'espère que ça t'as aidé.
Soit dit en passant, inject
de Ruby et reduce
de Clojure sont foldl
(ou foldl1
, Selon la version que vous utilisez). Habituellement, lorsqu'il n'y a qu'une seule forme dans un langage, c'est un pli gauche, y compris reduce
de Python, List::Util::reduce
De Perl, accumulate
de C++, Aggregate
de C # , inject:into:
De Smalltalk, array_reduce
De PHP, Fold
de Mathematica, etc. reduce
par défaut de LISP par défaut est le pli gauche mais il y a une option pour le pli droit.
Comme le souligne Konrad , leur sémantique est différente. Ils n'ont même pas le même type:
ghci> :t foldr
foldr :: (a -> b -> b) -> b -> [a] -> b
ghci> :t foldl
foldl :: (a -> b -> a) -> a -> [b] -> a
ghci>
Par exemple, l'opérateur d'ajout de liste (++) peut être implémenté avec foldr
comme
(++) = flip (foldr (:))
tandis que
(++) = flip (foldl (:))
vous donnera une erreur de type.