web-dev-qa-db-fra.com

Gauche et Droite se pliant sur une liste infinie

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?

69
TheIronKnuckle

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.

84
hammar

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.

17
Dan Burton

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 foldlet foldrcommencent toujours par un côté ou par l’autre, vous n’avez donc pas besoin de donner une longueur. 

foldra 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 foldrun "" (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 dumbFuncon 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 Truename__. Nous pouvons utiliser cela pour définir la fonction d'ordre supérieur anyqui renvoie Truesi 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 etel que f e == True

Par contre, ce n'est pas le cas de foldlname__. Pourquoi? Eh bien, un très simple foldlressemble à

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 foldlsur foldrmais pas l'inverse. Cela signifie que, d'une certaine manière, foldrest 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 foldlparfois, car vous can implémenterez récursivement foldlet en tirerez un gain de performances. 

11
Philip JF

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.

0
xuesheng