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!foldl
«en arrière» etfoldr
de «en avant» problématique. Ceci est en partie dû auflip
fait qu'il est appliqué(:)
dans l'illustration de la raison pour laquelle le pli est en arrière. La réaction naturelle est, "bien sûr, c'est à l'envers: vousflip
pédalez la concaténation de liste!" Il est également étrange de voir ce qu'on appelle «en arrière» puisquefoldl
s'appliquef
au premier élément de la liste en premier (le plus profond) dans une évaluation complète. C'estfoldr
que "marche en arrière", s'appliquantf
d'abord au dernier élément.foldl
etfoldr
et ignorer la rigueur et les optimisations, signifie d'abord "le plus extérieur", pas "le plus profond". C'est pourquoifoldr
peut traiter des listes infinies etfoldl
ne peut pas - le pli de droite s'appliquef
d'abord au premier élément de la liste et au résultat (non évalué) du pliage de la queue, tandis que le pli de gauche doit parcourir toute la liste pour évaluer l'application la plus externe def
.last xs = foldl (\a z-> z) undefined xs
.etc.
Intuitivement, il
foldl
est toujours à «l'extérieur» ou à «gauche» afin qu'il s'agrandisse en premier. À l'infini.la source
Vous pouvez voir dans la documentation de Haskell ici que foldl est récursif et ne se terminera jamais s'il est passé une liste infinie, car il s'appelle sur le paramètre suivant avant de renvoyer une valeur ...
la source
Je ne connais pas Haskell, mais dans Scheme,
fold-right
il «agira» toujours d'abord sur le dernier élément d'une liste. Ainsi, cela ne fonctionnera pas pour une liste cyclique (qui est identique à une liste infinie).Je ne sais pas si
fold-right
peut être écrit tail-recursive, mais pour toute liste cyclique, vous devriez obtenir un débordement de pile.fold-left
OTOH est normalement implémenté avec la récursivité de la queue, et restera simplement coincé dans une boucle infinie, si ce n'est pas le terminer tôt.la source