Auparavant, Nicolas Rinaudo a répondu à ma question sur Scala's List foldRight Toujours en utilisant foldLeft?
En étudiant Haskell actuellement, je crois comprendre que foldRight
devrait être préféré à foldLeft
dans les cas où ::
(prépend) peut être utilisé par rapport à ++
(append).
Si je comprends bien, la raison en est la performance - la première se produit dans O(1)
, c’est-à-dire ajoute un élément à l’avant - le temps constant. Alors que ce dernier requiert O(N)
, c’est-à-dire parcourir toute la liste et ajouter un élément.
En Scala, étant donné que foldLeft
est implémenté en termes de foldRight
, l'avantage d'utiliser :+
sur ++
avec foldRight
est-il important puisque foldRight
est inversé, puis foldLeft'd
?
A titre d'exemple, considérons cette opération fold..
simple pour renvoyer simplement les éléments d'une liste dans l'ordre.
foldLeft
se plie sur chaque élément, en ajoutant chaque élément à la liste via :+
.
scala> List("foo", "bar").foldLeft(List[String]()) {
(acc, elem) => acc :+ elem }
res9: List[String] = List(foo, bar)
foldRight
effectue un foldLeft avec l'opérateur ::
sur chaque élément, puis l'inverse.
scala> List("foo", "bar").foldRight(List[String]()) {
(elem, acc) => elem :: acc }
res10: List[String] = List(foo, bar)
En réalité, est-il important dans Scala de choisir foldLeft
ou foldRight
étant donné que foldRight
utilise foldRight
?
La réponse de @Rein Henrichs est en effet hors de propos pour Scala, car la mise en œuvre par Scala de foldLeft
et foldRight
est complètement différente (pour commencer, Scala a une évaluation enthousiaste).
foldLeft
et foldRight
ont eux-mêmes très peu à faire pour la performance de votre programme. Les deux sont (en termes généraux) O (n * c_f) où c_f est la complexité d’un appel à la fonction f
donné. foldRight
est plus lent avec un facteur constant à cause de la reverse
supplémentaire, cependant.
Le facteur réel qui différencie l’un de l’autre est donc la complexité de la fonction anonyme que vous donnez. Parfois, il est plus facile d’écrire une fonction efficace conçue pour fonctionner avec foldLeft
et parfois avec foldRight
. Dans votre exemple, la version foldRight
est la meilleure, car la fonction anonyme que vous donnez à foldRight
est O (1). En revanche, la fonction anonyme que vous attribuez à foldLeft
est O(n) lui-même (amortie, ce qui compte ici), car acc
continue de passer de 0 à n-1 et s'ajoute à une liste de n éléments est O (n).
Donc, en fait est important si vous choisissez foldLeft
ou foldRight
, mais pas à cause de ces fonctions elles-mêmes, mais à cause des fonctions anonymes qui leur ont été attribuées. Si les deux sont équivalents, choisissez foldLeft
par défaut.
Je peux fournir une réponse à Haskell, mais je doute que cela soit pertinent pour Scala:
Commençons par la source pour les deux,
foldl f z [] = z
foldl f z (x:xs) = foldl f (f z x) xs
foldr f z [] = z
foldr f z (x:xs) = f x (foldr f z xs)
Voyons maintenant où l’appel récursif de foldl ou foldr apparaît sur le côté droit. Pour foldl, c'est le plus extérieur. Pour foldr, c'est dans l'application de f. Cela a quelques implications importantes:
Si f
est un constructeur de données, ce constructeur de données sera le plus à gauche, le plus à l'extérieur avec foldr. Cela signifie que foldr implémente guarded récursivité , de sorte que les possibilités suivantes soient possibles:
> take 5 . foldr (:) [] $ [1..]
[1,2,3,4]
Cela signifie que, par exemple, foldr peut être à la fois un bon producteur et un bon consommateur pour une fusion abrégée . (Oui, foldr (:) []
est un morphisme d'identité pour les listes.)
Ce n'est pas possible avec foldl car le constructeur sera dans l'appel récursif à foldl et ne peut pas être mis en correspondance de motif.
Inversement, étant donné que l'appel récursif de foldl est situé dans l'extrême gauche, il sera réduit par une évaluation paresseuse et ne prendra pas de place sur la pile de filtrage de motifs. Combiné à une annotation de stricte rigueur (par exemple, foldl'
), cela permet à des fonctions telles que sum
ou length
de s'exécuter dans un espace constant.
Pour plus d'informations à ce sujet, voir Lazy Evaluation of Haskell .
Le fait d'utiliser foldLeft
ou foldRight
dans Scala, au moins avec des listes, au moins dans l'implémentation par défaut, importe en réalité. Je crois cependant que cette réponse n’est pas valable pour des bibliothèques telles que scalaz.
Si vous examinez le code source de foldLeft
et foldRight
pour LinearSeqOptimized , vous verrez que:
foldLeft
est implémenté avec une boucle et des variables mutables locales, et s'insère dans un cadre de pile.foldRight
est récursif, mais pas récursif, et consomme donc un cadre de pile par élément de la liste.foldLeft
est donc sûr, alors que foldRight
peut empiler le débordement pour les longues listes.
Edit Pour compléter ma réponse, car elle ne répond qu’à une partie de votre question: c’est également celle que vous utilisez qui dépend de ce que vous avez l’intention de faire.
Pour prendre votre exemple, ce que je considère comme la solution optimale consiste à utiliser foldLeft
, en ajoutant les résultats par avance à votre accumulateur, et reverse
au résultat.
Par ici:
C’est essentiellement ce que vous pensiez faire avec foldRight
en supposant que cela a été implémenté en termes de foldLeft
.
Si vous utilisez foldRight
, vous obtiendrez une implémentation légèrement plus rapide (enfin, légèrement ... deux fois plus rapide, vraiment, mais toujours O(n)) au détriment de la sécurité.
On pourrait argumenter que si vous savez que vos listes seront suffisamment petites, il n’y aura pas de problème de sécurité et vous pourrez utiliser foldRight
. Je pense, mais ce n'est qu'un avis, que si vos listes sont suffisamment petites pour que vous n'ayez pas à vous soucier de votre pile, elles sont suffisamment petites pour que vous n'ayez pas à vous soucier de la perte de performances.
Cela dépend, considérez ce qui suit:
scala> val l = List(1, 2, 3)
l: List[Int] = List(1, 2, 3)
scala> l.foldLeft(List.empty[Int]) { (acc, ele) => ele :: acc }
res0: List[Int] = List(3, 2, 1)
scala> l.foldRight(List.empty[Int]) { (ele, acc) => ele :: acc }
res1: List[Int] = List(1, 2, 3)
Comme vous pouvez le constater, foldLeft
parcourt la liste de head
au dernier élément. foldRight
quant à lui le traverse du dernier élément à head
.
Si vous utilisez le pliage pour l'agrégation, il ne devrait y avoir aucune différence:
scala> l.foldLeft(0) { (acc, ele) => ele + acc }
res2: Int = 6
scala> l.foldRight(0) { (ele, acc) => ele + acc }
res3: Int = 6
Je ne suis pas un expert en Scala, mais dans Haskell, l’un des éléments de différenciation les plus importants entre foldl'
(qui devrait être le pli gauche par défaut) et foldr
est que foldr
fonctionnera sur des structures de données infinies, où foldl'
sera suspendu indéfiniment.
Afin de comprendre pourquoi, il est recommandé de visiter foldl.com et foldr.com , d’élargir les évaluations plusieurs fois et de reconstruire l’arborescence des appels. Vous verrez rapidement où foldr
est approprié par rapport à foldl'
.
scala> val words = List("Hic", "Est", "Index")
words: List[String] = List(Hic, Est, Index)
Cas de foldLeft: Les éléments de la liste s'ajouteront à la chaîne vide en premier et suivis
words.foldLeft("")(_ + _) == (("" + "Hic") + "Est") + "Index" //"HicEstIndex"
Cas de foldRight: Une chaîne vide s'ajoute à la fin des éléments
words.foldRight("")(_ + _) == "Hic" + ("Est" + ("Index" + "")) //"HicEstIndex"
Les deux cas renverront la même sortie
def foldRight[B](z: B)(f: (A, B) => B): B
def foldLeft[B](z: B)(f: (B, A) => B): B