Le code de la fonction myAny dans cette question utilise foldr. Il arrête de traiter une liste infinie lorsque le prédicat est satisfait.
Je l'ai réécrit en utilisant foldl:
myAny :: (a -> Bool) -> [a] -> Bool
myAny p list = foldl step False list
where
step acc item = p item || acc
(Notez que les arguments de la fonction step sont correctement inversés.)
Cependant, il n'arrête plus de traiter des listes infinies.
J'ai tenté de retracer l'exécution de la fonction comme dans réponse d'Apocalisp :
myAny even [1..]
foldl step False [1..]
step (foldl step False [2..]) 1
even 1 || (foldl step False [2..])
False || (foldl step False [2..])
foldl step False [2..]
step (foldl step False [3..]) 2
even 2 || (foldl step False [3..])
True || (foldl step False [3..])
True
Cependant, ce n'est pas ainsi que la fonction se comporte. Comment est-ce mal?
La différence entre fold
s semble être une source fréquente de confusion, voici donc un aperçu plus général:
Pensez à plier une liste de n valeurs [x1, x2, x3, x4 ... xn ]
Avec une fonction f
et une graine z
.
foldl
est:f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
foldl (flip (:)) []
inverse une liste.foldr
est:f x1 (f x2 (f x3 (f x4 ... (f xn z) ... )))
f
à la valeur suivante et au résultat du pliage du reste de la liste.foldr (:) []
renvoie une liste inchangée.Il y a un point légèrement subtil ici qui déclenche parfois les gens: parce que foldl
est à l'envers chaque application de f
est ajoutée au extérieur du résultat; et parce qu'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 itère d'abord à travers la liste entière en construisant une expression des applications de fonction imbriquées, puis évalue la fonction la plus externe, évaluer ses arguments au besoin. Si f
utilise toujours son premier argument, cela signifie que Haskell doit reculer jusqu'au terme le plus profond, puis travailler en arrière pour calculer chaque application de f
.
C'est évidemment loin de la récursivité efficace que la plupart des programmeurs fonctionnels connaissent et adorent!
En fait, même si foldl
est techniquement récursif, car l'expression de résultat entière est construite avant d'évaluer quoi que ce soit, foldl
peut provoquer un débordement de pile!
En revanche, considérons foldr
. C'est aussi paresseux, mais parce qu'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 single, dont le deuxième argument est le reste de la liste repliée. Si f
est paresseux dans son deuxième argument - un constructeur de données, par exemple - le résultat sera incrémentalement paresseux , avec chaque étape du pli 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 quand foldl
ne fonctionne pas: le premier peut paresseusement convertir 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 une 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 foldl
, construisant un é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. Vous avez peut-être remarqué qu'il semble que foldr
peut tout faire foldl
peut, 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 sur une grande liste (mais pas infinie)? Pour cela, nous voulons un repli strict , qui les bibliothèques standard fournissent bien :
foldl'
Est:f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
foldl' (flip (:)) []
inverse une liste.Parce que foldl'
Est strict , pour calculer le résultat, Haskell va évaluerf
à 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 ne page qui en discute .
myAny even [1..]
foldl step False [1..]
foldl step (step False 1) [2..]
foldl step (step (step False 1) 2) [3..]
foldl step (step (step (step False 1) 2) 3) [4..]
etc.
Intuitivement, foldl
est toujours à "l'extérieur" ou à "gauche", il est donc développé en premier. À l'infini.
Vous pouvez voir dans la documentation de Haskell ici que foldl est récursif et ne se terminera jamais si passé une liste infinie, car il s'appelle sur le paramètre suivant avant de renvoyer une valeur ...
Je ne connais pas Haskell, mais dans Scheme, fold-right
agira toujours en premier sur le dernier élément d'une liste. Ainsi, cela ne fonctionnera pas pour la 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 une récursion de queue, et restera coincé dans une boucle infinie, s'il ne le termine pas tôt.