La fold
différence semble être une source fréquente de confusion, voici donc un aperçu plus général:
Envisagez de plier une liste de n valeurs [x1, x2, x3, x4 ... xn ]
avec une fonction f
et une valeur de départ z
.
foldl
est:
- Associatif gauche :
f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
- Queue récursive : elle parcourt la liste, produisant ensuite la valeur
- Paresseux : rien n'est évalué tant que le résultat n'est pas nécessaire
- En arrière :
foldl (flip (:)) []
inverse une liste.
foldr
est:
- Associatif droit :
f x1 (f x2 (f x3 (f x4 ... (f xn z) ... )))
- Récursif en argument : chaque itération s'applique
f
à la valeur suivante et au résultat du pliage du reste de la liste.
- Paresseux : rien n'est évalué tant que le résultat n'est pas nécessaire
- En avant :
foldr (:) []
renvoie une liste inchangée.
Il y a un point un peu subtile ici que les gens voyages parfois: Parce que foldl
est en arrière chaque application de f
est ajouté à l' extérieur du résultat; et comme il est paresseux , rien n'est évalué tant que le résultat n'est pas requis. Cela signifie que pour calculer n'importe quelle partie du résultat, Haskell effectue d'abord une itération dans toute la liste en construisant une expression d'applications de fonction imbriquées, puis évalue la fonction la plus externe , évaluant ses arguments si nécessaire. Si f
utilise toujours son premier argument, cela signifie que Haskell doit récurer jusqu'au terme le plus profond, puis travailler à rebours en calculant chaque application de f
.
C'est évidemment loin de la récursivité de queue efficace que la plupart des programmeurs fonctionnels connaissent et aiment!
En fait, même si elle foldl
est techniquement récursive en queue, parce que l'expression de résultat entière est construite avant d'évaluer quoi que ce soit, foldl
peut provoquer un débordement de pile!
D'un autre côté, considérez foldr
. C'est aussi paresseux, mais comme il s'exécute vers l'avant , chaque application de f
est ajoutée à l' intérieur du résultat. Ainsi, pour calculer le résultat, Haskell construit une application de fonction unique , dont le deuxième argument est le reste de la liste pliée. Si f
est paresseux dans son deuxième argument - un constructeur de données, par exemple - le résultat sera paresseux de manière incrémentielle , chaque étape du repli étant calculée uniquement lorsqu'une partie du résultat qui en a besoin est évaluée.
Nous pouvons donc voir pourquoi foldr
fonctionne parfois sur des listes infinies alors que ce foldl
n'est pas le cas: le premier peut convertir paresseusement une liste infinie en une autre structure de données infinie paresseuse, tandis que le second doit inspecter la liste entière pour générer n'importe quelle partie du résultat. D'un autre côté, foldr
avec une fonction qui a besoin des deux arguments immédiatement, comme (+)
, fonctionne (ou plutôt ne fonctionne pas) un peu comme la foldl
construction d'une énorme expression avant de l'évaluer.
Les deux points importants à noter sont donc les suivants:
foldr
peut transformer une structure de données récursive paresseuse en une autre.
- Sinon, les plis paresseux planteront avec un débordement de pile sur des listes volumineuses ou infinies.
Vous avez peut-être remarqué qu'il semble que tout foldr
peut faire foldl
, et plus encore. C'est vrai! En fait, foldl est presque inutile!
Mais que se passe-t-il si nous voulons produire un résultat non paresseux en repliant une grande liste (mais pas infinie)? Pour cela, nous voulons un pli strict , que les bibliothèques standard fournissent bien :
foldl'
est:
- Associatif gauche :
f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
- Queue récursive : elle parcourt la liste, produisant ensuite la valeur
- Strict : chaque application de fonction est évaluée en cours de route
- En arrière :
foldl' (flip (:)) []
inverse une liste.
Parce que foldl'
c'est strict , pour calculer le résultat, Haskell évaluera f
à chaque étape, au lieu de laisser l'argument de gauche accumuler une énorme expression non évaluée. Cela nous donne la récursion de queue habituelle et efficace que nous voulons! En d'autres termes:
foldl'
peut plier efficacement de grandes listes.
foldl'
se bloque dans une boucle infinie (ne provoque pas de débordement de pile) sur une liste infinie.
Le wiki Haskell a également une page traitant de cela .
foldr
est meilleur quefoldl
dans Haskell , alors que l'inverse est vrai dans Erlang (que j'ai appris avant Haskell ). Depuis Erlang n'est pas paresseux et les fonctions ne sont pas curry , doncfoldl
dans Erlang se comporte commefoldl'
ci-dessus. C'est une excellente réponse! Bon travail et merci!