91 votes

Qu'est-ce qu'il y a de si mal avec Lazy I/O ?

J'ai généralement entendu dire que le code de production devrait éviter l'utilisation de Lazy I/O. Ma question est la suivante : pourquoi ? Est-il jamais acceptable d'utiliser Lazy I/O en dehors d'un simple jeu ? Et qu'est-ce qui rend les alternatives (par exemple, les énumérateurs) meilleures ?

82voto

Don Stewart Points 94361

Le problème de la Lazy IO est que la libération de la 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". Lorsque votre programme abandonne la dernière référence à la ressource, le GC finira par s'exécuter et libérer cette ressource.

Les flux paresseux sont une très style pratique pour la programmation. C'est pourquoi les pipes à coquille sont si amusantes et populaires.

Cependant, si les ressources sont limitées (comme dans les scénarios de haute performance, ou les environnements de production qui s'attendent à évoluer jusqu'aux limites de la machine), compter sur le GC pour faire le ménage peut être une garantie insuffisante.

Il est parfois nécessaire de libérer les ressources avec empressement, afin d'améliorer l'évolutivité.

Quelles sont donc les alternatives à l'IO paresseux qui n'impliquent pas l'abandon du traitement incrémental (qui consommerait à son tour trop de ressources) ? Eh bien, nous avons foldl le traitement basé sur les itérations ou les énumérateurs, introduit par Oleg Kiselyov à la fin des années 2000 et popularisé depuis par un certain nombre de projets basés sur la mise en réseau.

Au lieu de traiter les données sous forme de flux paresseux ou en un seul gros lot, nous nous abstrayons sur un traitement strict basé sur les morceaux, avec une finalisation garantie de la ressource une fois le dernier morceau lu. C'est l'essence même de la programmation par itération, qui offre de très belles contraintes de ressources.

L'inconvénient de l'IO basée sur l'itération est qu'elle a un modèle de programmation quelque peu maladroit (à peu près analogue à la programmation basée sur les événements, par opposition à un contrôle agréable basé sur les fils). 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, l'IO paresseux est tout à fait satisfaisant. Cependant, si vous êtes amené à ouvrir de nombreux fichiers, à parler sur de nombreuses sockets, ou à utiliser de nombreuses ressources simultanées, une approche itérative (ou énumératrice) peut être utile.

41voto

John L Points 20989

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 itérations : elles facilitent le raisonnement sur la gestion de l'espace parce que les anciennes données doivent être explicitement conservées. Pensez-y :

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

Il s'agit d'une fuite d'espace bien connue, car la liste entière xs doit être conservé en mémoire pour calculer les deux sum y 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 il est quelque peu incommode de devoir le faire pour chaque processeur de flux. Il existe quelques généralisations ( Conal Elliott - Un beau pliage en accordéon ), mais ils ne semblent pas avoir pris le dessus. Cependant, les itérés peuvent vous permettre d'atteindre un niveau d'expression similaire.

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

Ce n'est pas aussi efficace qu'un pliage car la liste est toujours itérée plusieurs fois, mais elle est collectée par morceaux, de sorte que les anciennes données peuvent être efficacement récupérées. Afin de briser cette propriété, il est nécessaire de conserver explicitement l'entrée entière, comme avec stream2list :

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

L'état des itérations en tant que modèle de programmation est un travail en cours, mais il est bien meilleur qu'il y a un an. Nous apprenons quels combinateurs sont utiles (par ex. zip , breakE , enumWith ) et ceux qui le sont moins, avec pour résultat que les itérations et combinateurs intégrés offrent une expressivité toujours plus grande.

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.

26voto

augustss Points 15750

J'utilise lazy I/O dans le code de production tout le temps. C'est seulement un problème dans certaines circonstances, comme Don l'a mentionné. Mais pour la simple lecture de quelques fichiers, cela fonctionne très bien.

21voto

Petr Pudlák Points 25113

Mise à jour : Récemment sur haskell-cafe Oleg Kiseljov a montré que unsafeInterleaveST (qui est utilisé pour implémenter l'entrée-sortie paresseuse dans la monade ST) n'est pas du tout sûr - il brise le raisonnement équationnel. Il montre qu'il permet de construire bad_ctx :: ((Bool,Bool) -> Bool) -> Bool de telle sorte que

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

même si == est commutatif.


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

Par exemple, une erreur courante des débutants consiste à 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 méthode consiste à 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 inattendu et une erreur facile à commettre.


Voir aussi : Trois exemples de problèmes avec Lazy I/O .

18voto

Ben Millwood Points 4020

Un autre problème de l'IO paresseux qui n'a pas été mentionné jusqu'à présent est qu'il a un comportement surprenant. Dans un programme Haskell normal, il peut parfois être difficile de prévoir quand chaque partie de votre programme est évaluée, mais heureusement, en raison de la pureté, cela n'a pas vraiment d'importance, sauf si vous avez des problèmes de performance. Lorsque l'IO paresseux est introduit, l'ordre d'évaluation de votre code a en fait un effet sur sa signification, de sorte que des 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 un code qui semble raisonnable mais qui est rendu plus confus par les entrées-sorties 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 les OI paresseux, à moins qu'il y ait un réel problème à faire tout le travail en amont.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X