web-dev-qa-db-fra.com

foldLeft v. foldRight - est-ce important?

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?

17
Kevin Meredith

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.

35
sjrd

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:

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

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

12
Rein Henrichs

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:

  • toute l'opération est O (n)
  • il ne débordera pas la pile, quelle que soit la taille de la liste

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.

8
Nicolas Rinaudo

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
4
tgr

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

1
Benjamin Kovach
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
0
Thirupathi Chavati