J'ai des problèmes avec le passage suivant de Learn You A Haskell (Great book imo, ne le dissing pas):
Une grande différence est que les plis droits Fonctionnent sur des listes infinies, alors que ceux de gauche ne fonctionnent pas! En termes clairs, si vous prenez une liste infinie à un moment donné et que vous la repliez vers le haut En partant de la droite, vous arriverez finalement au début de la liste. Cependant , si vous prenez une liste infinie en un point et que vous essayez de la plier par la gauche, vous n’atteindrez jamais la fin!
Je ne comprends pas ça. Si vous prenez une liste infinie et essayez de la replier de la droite, vous devrez commencer au point à l'infini, ce qui n'est tout simplement pas le cas (Si quelqu'un connaît un langage où vous pouvez le faire, dites: p ). Au moins, vous devriez commencer par là en fonction de l'implémentation de Haskell, car dans Haskell, foldr et foldl ne prennent pas d'argument qui détermine où dans la liste ils doivent commencer à se replier.
Je suis d’accord avec la citation iff foldr et foldl ont pris des arguments qui déterminaient où dans la liste ils devaient commencer le pliage, car il est logique que si vous prenez une liste infinie et commencez à plier directement à partir d’un index défini, il will _ se termine éventuellement, alors que peu importe où vous commencez avec un pli gauche; vous vous replierez vers l'infini. Cependant foldr et foldl ne pas prennent cet argument, et par conséquent la citation n’a aucun sens. En Haskell, un pli gauche et un pli droit sur une liste infinie ne termineront pas .
Ma compréhension est-elle correcte ou manque-t-il quelque chose?
La clé ici est la paresse. Si la fonction que vous utilisez pour plier la liste est stricte, aucun pli gauche ni droit ne se terminera, étant donné une liste infinie.
Prelude> foldr (+) 0 [1..]
^CInterrupted.
Toutefois, si vous essayez de plier une fonction moins stricte, vous pouvez obtenir un résultat final.
Prelude> foldr (\x y -> x) 0 [1..]
1
Vous pouvez même obtenir un résultat qui est une structure de données infinie. Ainsi, si elle ne se termine pas dans un sens, elle est toujours capable de produire un résultat qui peut être consommé paresseusement.
Prelude> take 10 $ foldr (:) [] [1..]
[1,2,3,4,5,6,7,8,9,10]
Cependant, cela ne fonctionnera pas avec foldl
, car vous ne pourrez jamais évaluer l'appel de fonction le plus externe, qu'il soit paresseux ou pas.
Prelude> foldl (flip (:)) [] [1..]
^CInterrupted.
Prelude> foldl (\x y -> y) 0 [1..]
^CInterrupted.
Notez que la différence clé entre un pli gauche et un pli droit n'est pas l'ordre dans lequel la liste est parcourue, qui est toujours de gauche à droite, mais plutôt la manière dont les applications de fonction résultantes sont imbriquées.
Avec foldr
, ils sont imbriqués "à l'intérieur"
foldr f y (x:xs) = f x (foldr f y xs)
Ici, la première itération entraînera l'application la plus externe de f
. Ainsi, f
a la possibilité d’être paresseux afin que le deuxième argument ne soit pas toujours évalué ou qu’il puisse produire une partie d’une structure de données sans forcer son deuxième argument.
Avec foldl
, ils sont imbriqués "à l'extérieur"
foldl f y (x:xs) = foldl f (f y x) xs
Ici, nous ne pouvons rien évaluer avant d'avoir atteint l'application la plus externe de f
, que nous n'atteindrons jamais dans le cas d'une liste infinie, que f
soit strict ou non.
La phrase clé est "à un moment donné".
si vous prenez une liste infinie à un moment donné et que vous la pliez en partant de la droite, vous arriverez au début de la liste.
Donc, vous avez raison, vous ne pouvez probablement pas commencer par le "dernier" élément d'une liste infinie. Mais le propos de l'auteur est le suivant: supposons que vous puissiez le faire. Il suffit de choisir un point très loin (pour les ingénieurs, c’est «assez proche» de l’infini) et de commencer à plier vers la gauche. Vous finissez par vous retrouver au début de la liste. Il n'en va pas de même pour le pli gauche, si vous choisissez un point waaaay (et appelez-le "assez près" du début de la liste), et commencez à vous replier vers la droite, vous avez toujours un chemin infini à parcourir.
Le truc, c'est que parfois vous n'avez pas besoin d'aller à l'infini. Vous n’aurez peut-être même pas besoin d’y aller. Mais vous ne savez peut-être pas à quelle distance vous devez aller au préalable, auquel cas des listes infinies sont assez pratiques.
L'illustration simple est foldr (:) [] [1..]
. Faisons le pli.
Rappelez-vous que foldr f z (x:xs) = f x (foldr f z xs)
. Sur une liste infinie, peu importe ce que z
est, je le garde juste comme z
au lieu de []
qui encombre l'illustration.
foldr (:) z (1:[2..]) ==> (:) 1 (foldr (:) z [2..])
1 : foldr (:) z (2:[3..]) ==> 1 : (:) 2 (foldr (:) z [3..])
1 : 2 : foldr (:) z (3:[4..]) ==> 1 : 2 : (:) 3 (foldr (:) z [4..])
1 : 2 : 3 : ( lazily evaluated thunk - foldr (:) z [4..] )
Voyez comment foldr
, bien qu’il soit théoriquement un repli du right , dans ce cas, génère des éléments individuels de la liste résultante en commençant par le left ? Donc, si vous take 3
de cette liste, vous pouvez clairement voir qu'il sera capable de produire [1,2,3]
et qu'il n'est pas nécessaire d'évaluer le pli plus loin.
N'oubliez pas que dans Haskell, vous pouvez utiliser des listes infinies à cause d'une évaluation paresseuse. Donc, head [1..]
est juste 1, et head $ map (+1) [1..]
est 2, même si `[1 ..] est infiniment long. Si vous ne l'obtenez pas, arrêtez-vous et jouez avec pendant un moment. Si vous obtenez cela, lisez la suite ...
Je pense que votre confusion est en partie due au fait que foldl
et foldr
commencent toujours par un côté ou par l’autre, vous n’avez donc pas besoin de donner une longueur.
foldr
a une définition très simple
foldr _ z [] = z
foldr f z (x:xs) = f x $ foldr f z xs
pourquoi cela pourrait-il se terminer sur des listes infinies, essayez bien
dumbFunc :: a -> b -> String
dumbFunc _ _ = "always returns the same string"
testFold = foldr dumbFunc 0 [1..]
nous passons ici dans foldr
un "" (puisque la valeur importe peu) et la liste infinie de nombres naturels. Est-ce que cela se termine? Oui.
Elle se termine parce que l'évaluation de Haskell équivaut à une réécriture de terme paresseux.
Alors
testFold = foldr dumbFunc "" [1..]
devient (pour permettre la correspondance de motif)
testFold = foldr dumbFunc "" (1:[2..])
qui est le même que (de notre définition du pli)
testFold = dumbFunc 1 $ foldr dumbFunc "" [2..]
maintenant, par la définition de dumbFunc
on peut conclure
testFold = "always returns the same string"
C’est plus intéressant lorsque nous avons des fonctions qui font quelque chose, mais qui sont parfois paresseuses. Par exemple
foldr (||) False
est utilisé pour rechercher si une liste contient des éléments True
name__. Nous pouvons utiliser cela pour définir la fonction d'ordre supérieur any
qui renvoie True
si et seulement si la fonction transmise est vraie pour un élément de la liste
any :: (a -> Bool) -> [a] -> Bool
any f = (foldr (||) False) . (map f)
La bonne chose à propos de l’évaluation paresseuse, c’est que cela s’arrête quand il rencontre le premier élément e
tel que f e == True
Par contre, ce n'est pas le cas de foldl
name__. Pourquoi? Eh bien, un très simple foldl
ressemble à
foldl f z [] = z
foldl f z (x:xs) = foldl f (f z x) xs
Maintenant, que serait-il arrivé si nous avions essayé notre exemple ci-dessus
testFold' = foldl dumbFunc "" [1..]
testFold' = foldl dumbFunc "" (1:[2..])
cela devient maintenant:
testFold' = foldl dumbFunc (dumbFunc "" 1) [2..]
alors
testFold' = foldl dumbFunc (dumbFunc (dumbFunc "" 1) 2) [3..]
testFold' = foldl dumbFunc (dumbFunc (dumbFunc (dumbFunc "" 1) 2) 3) [4..]
testFold' = foldl dumbFunc (dumbFunc (dumbFunc (dumbFunc (dumbFunc "" 1) 2) 3) 4) [5..]
ainsi de suite. Nous ne pourrons jamais aller nulle part, car Haskell évalue toujours d'abord la fonction la plus externe (c'est-à-dire une évaluation paresseuse).
Une conséquence intéressante de ceci est que vous pouvez implémenter foldl
sur foldr
mais pas l'inverse. Cela signifie que, d'une certaine manière, foldr
est la plus fondamentale de toutes les fonctions de chaîne d'ordre supérieur, puisqu'il s'agit de celle que nous utilisons pour implémenter presque toutes les autres. Vous souhaiterez peut-être toujours utiliser un foldl
parfois, car vous can implémenterez récursivement foldl
et en tirerez un gain de performances.
Il y a une bonne explication simple sur Haskell wiki . Il présente une réduction progressive avec différents types de fonctions de pliage et d'accumulation.