Qu'y a-t-il de si mauvais à propos de Lazy I / O?

89

J'ai généralement entendu dire que le code de production devrait éviter d'utiliser Lazy I / O. Ma question est: pourquoi? Est-il toujours acceptable d'utiliser la Lazy I / O en dehors de simplement jouer? Et qu'est-ce qui améliore les alternatives (par exemple les agents recenseurs)?

Dan Burton
la source

Réponses:

81

Lazy IO a le problème que la libération de toute ressource que vous avez acquise est quelque peu imprévisible, car elle dépend de la façon dont votre programme consomme les données - son «modèle de demande». Une fois que votre programme supprime la dernière référence à la ressource, le GC finira par s'exécuter et libérer cette ressource.

Les flux paresseux sont un style très pratique pour programmer. C'est pourquoi les tubes shell sont si amusants et populaires.

Cependant, si les ressources sont limitées (comme dans les scénarios à hautes performances ou dans les environnements de production qui s'attendent à évoluer aux limites de la machine), le fait de se fier au GC pour nettoyer peut être une garantie insuffisante.

Parfois, vous devez libérer des ressources avec empressement, afin d'améliorer l'évolutivité.

Alors, quelles sont les alternatives aux IO paresseux qui ne signifient pas renoncer au traitement incrémentiel (qui à son tour consommerait trop de ressources)? Eh bien, nous avons foldlun traitement basé, aka iteratees ou énumérateurs, introduit par Oleg Kannedov à la fin des années 2000 , et depuis popularisé par un certain nombre de projets basés sur le réseautage.

Au lieu de traiter les données sous forme de flux paresseux, ou dans un lot énorme, nous faisons plutôt abstraction d'un traitement strict basé sur des blocs, avec une finalisation garantie de la ressource une fois que le dernier bloc est lu. C'est l'essence de la programmation iteratee, et celle qui offre de très belles contraintes de ressources.

L'inconvénient des E / S basées sur iteratee est qu'il a un modèle de programmation quelque peu gênant (à peu près analogue à la programmation basée sur les événements, par opposition à un contrôle basé sur les threads). C'est définitivement une technique avancée, dans n'importe quel langage de programmation. Et pour la grande majorité des problèmes de programmation, lazy IO est entièrement satisfaisante. Cependant, si vous allez ouvrir de nombreux fichiers, ou parler sur plusieurs sockets, ou utiliser autrement de nombreuses ressources simultanées, une approche iteratee (ou énumérateur) peut avoir du sens.

Don Stewart
la source
22
Puisque je viens de suivre un lien vers cette vieille question à partir d'une discussion sur les E / S paresseuses, j'ai pensé ajouter une note que depuis lors, une grande partie de la maladresse des iteratees a été remplacée par de nouvelles bibliothèques de streaming comme des tuyaux et des conduits .
Ørjan Johansen
40

Dons a fourni une très bonne réponse, mais il a laissé de côté ce qui est (pour moi) l'une des caractéristiques les plus convaincantes des iteratees: ils facilitent le raisonnement sur la gestion de l'espace car les anciennes données doivent être explicitement conservées. Considérer:

average :: [Float] -> Float
average xs = sum xs / length xs

Il s'agit d'une fuite d'espace bien connue, car la liste entière xsdoit être conservée en mémoire pour calculer à la fois sumet length. Il est possible de faire un consommateur efficace en créant un pli:

average2 :: [Float] -> Float
average2 xs = uncurry (/) <$> foldl (\(sumT, n) x -> (sumT+x, n+1)) (0,0) xs
-- N.B. this will build up thunks as written, use a strict pair and foldl'

Mais c'est quelque peu gênant de devoir faire cela pour chaque processeur de flux. Il y a quelques généralisations ( Conal Elliott - Beautiful Fold Zipping ), mais elles ne semblent pas avoir compris. Cependant, les iteratees peuvent vous procurer un niveau d'expression similaire.

aveIter = uncurry (/) <$> I.zip I.sum I.length

Ce n'est pas aussi efficace qu'un repli car la liste est toujours itérée plusieurs fois, mais elle est collectée par blocs afin que les anciennes données puissent être efficacement récupérées. Pour casser cette propriété, il est nécessaire de conserver explicitement toute l'entrée, comme avec stream2list:

badAveIter = (\xs -> sum xs / length xs) <$> I.stream2list

L'état des itérés en tant que modèle de programmation est un travail en cours, mais il est bien meilleur qu'il y a encore un an. Nous apprenons ce que combinateurs sont utiles (par exemple zip, breakE, enumWith) et qui le sont moins, avec le résultat qui a construit en iteratees et combinateurs fournissent sans cesse plus expressivité.

Cela dit, Dons a raison de dire qu'il s'agit d'une technique avancée; Je ne les utiliserais certainement pas pour tous les problèmes d'E / S.

John L
la source
25

J'utilise des E / S paresseuses dans le code de production tout le temps. Ce n'est un problème que dans certaines circonstances, comme Don l'a mentionné. Mais pour simplement lire quelques fichiers, cela fonctionne bien.

augustes
la source
J'utilise également des E / S paresseuses. Je me tourne vers iteratees lorsque je veux plus de contrôle sur la gestion des ressources.
John L
20

Mise à jour: Récemment, sur haskell-cafe, Oleg Kiseljov a montré que unsafeInterleaveST(qui est utilisé pour implémenter des IO paresseux dans la monade ST) est très dangereux - cela brise le raisonnement équationnel. Il montre que cela permet de construire de bad_ctx :: ((Bool,Bool) -> Bool) -> Bool telle sorte que

> bad_ctx (\(x,y) -> x == y)
True
> bad_ctx (\(x,y) -> y == x)
False

même si elle ==est commutative.


Un autre problème avec les E / S différées: l'opération d'E / S réelle peut être différée jusqu'à ce qu'il soit trop tard, par exemple après la fermeture du fichier. Citation de Haskell Wiki - Problèmes avec IO paresseux :

Par exemple, une erreur courante des débutants est de fermer un fichier avant d'avoir fini de le lire:

wrong = do
    fileData <- withFile "test.txt" ReadMode hGetContents
    putStr fileData

Le problème est que withFile ferme le handle avant que fileData ne soit forcé. La bonne façon est de passer tout le code à withFile:

right = withFile "test.txt" ReadMode $ \handle -> do
    fileData <- hGetContents handle
    putStr fileData

Ici, les données sont consommées avant la fin de withFile.

C'est souvent une erreur inattendue et facile à faire.


Voir aussi: Trois exemples de problèmes avec E / S Lazy .

Petr Pudlák
la source
En fait, combiner hGetContentset withFileest inutile car le premier met la poignée dans un état "pseudo-fermé" et gérera la fermeture pour vous (paresseusement) de sorte que le code est exactement équivalent à readFile, ou même openFilesans hClose. C'est essentiellement ce paresseux I / O est . Si vous ne l' utilisez pas readFile, getContentsou hGetContentsvous ne l' utilisez E / S paresseux. Par exemple line <- withFile "test.txt" ReadMode hGetLinefonctionne très bien.
Dag
1
@Dag: bien que hGetContentsgérera la fermeture du fichier pour vous, il est également permis de le fermer vous-même «tôt», et permet de garantir que les ressources sont libérées de manière prévisible.
Ben Millwood
17

Un autre problème avec lazy IO qui n'a pas été mentionné jusqu'à présent est son comportement surprenant. Dans un programme Haskell normal, il peut parfois être difficile de prédire quand chaque partie de votre programme est évaluée, mais heureusement en raison de sa pureté, cela n'a pas vraiment d'importance à moins que vous ayez des problèmes de performances. Lorsqu'une IO paresseuse est introduite, l'ordre d'évaluation de votre code a en fait un effet sur sa signification, de sorte que les changements que vous avez l'habitude de considérer comme inoffensifs peuvent vous causer de véritables problèmes.

À titre d'exemple, voici une question sur le code qui semble raisonnable mais qui est rendue plus confuse par les E / S différées: withFile vs openFile

Ces problèmes ne sont pas toujours fatals, mais c'est une autre chose à laquelle il faut penser, et un mal de tête suffisamment grave pour que j'évite personnellement IO paresseux à moins qu'il y ait un réel problème à faire tout le travail d'avance.

Ben Millwood
la source