web-dev-qa-db-fra.com

comportement foldl versus foldr avec des listes infinies

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?

110
titaniumdecoy

La différence entre folds 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:

  • Associatif gauche : f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
  • Tail récursif : il parcourt la liste, produisant ensuite la valeur
  • Paresseux : Rien n'est évalué tant que le résultat n'est pas nécessaire
  • Vers l'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 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
  • Vers l'avant : 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.
  • Sinon, les plis paresseux planteront avec un débordement de pile sur des listes grandes ou infinies.

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:

  • Associatif gauche : f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
  • Tail récursif : il parcourt la liste, produisant ensuite la valeur
  • Strict : Chaque application de fonction est évaluée en cours de route
  • Vers l'arrière : 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 .

215
C. A. McCann
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.

26
Artelius

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

10
Romain

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.

0
leppie