J'ai généralement entendu que le code de production devrait éviter d'utiliser les E/S paresseuses. Ma question est, pourquoi? Est-il jamais acceptable d'utiliser des E/S paresseuses en dehors de simplement jouer? Et qu'est-ce qui rend les alternatives (par exemple les enquêteurs) meilleures?
Lazy IO a le problème de libérer quelque ressource que vous avez acquise est quelque peu imprévisible, car cela dépend de la façon dont votre programme consomme les données - son "modèle de demande". Une fois que votre programme a supprimé la dernière référence à la ressource, le GC finira par exécuter et libérer cette ressource.
Les flux paresseux sont un très style pratique à programmer. C'est pourquoi les pipes Shell sont si amusantes et populaires.
Cependant, si les ressources sont limitées (comme dans les scénarios à hautes performances ou les environnements de production qui s'attendent à évoluer jusqu'aux limites de la machine), le recours au GC pour nettoyer peut être une garantie insuffisante.
Parfois, vous devez libérer les ressources avec impatience, afin d'améliorer l'évolutivité.
Alors, quelles sont les alternatives à lazy IO qui ne signifient pas abandonner le traitement incrémentiel (qui à son tour consommerait trop de ressources)? Eh bien, nous avons un traitement basé sur foldl
, alias itérés ou énumérateurs, introduits par Oleg Kiselyov à la fin des années 20, et depuis popularisés par un certain nombre de réseaux projets.
Au lieu de traiter les données sous forme de flux paresseux, ou dans un lot énorme, nous préférons plutôt un traitement strict basé sur des blocs, avec une finalisation garantie de la ressource une fois le dernier bloc lu. C'est l'essence même de la programmation itérative, et qui offre des contraintes de ressources très agréables.
L'inconvénient de l'itérate IO est qu'il a un modèle de programmation quelque peu maladroit (à peu près analogue à la programmation basée sur les événements, par rapport au contrôle basé sur les threads Nice). C'est certainement une technique avancée, en n'importe quel langage de programmation. Et pour la grande majorité des problèmes de programmation, lazy IO est entièrement satisfaisant. Cependant, si vous allez ouvrir de nombreux fichiers, ou parler sur de nombreux sockets, ou autrement utiliser de nombreuses ressources simultanées , une approche itérative (ou énumérateur) pourrait avoir un sens.
Dons a fourni une très bonne réponse, mais il a omis ce qui est (pour moi) l'une des caractéristiques les plus convaincantes des itérés: ils facilitent la réflexion 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 xs
doit être conservée en mémoire pour calculer à la fois sum
et 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 gênant de devoir le faire pour chaque processeur de flux. Il y a quelques généralisations ( Conal Elliott - Beautiful Fold Zipping ), mais elles ne semblent pas avoir fait leur chemin. Cependant, les itérés peuvent vous obtenir un niveau d'expression similaire.
aveIter = uncurry (/) <$> I.Zip I.sum I.length
Ce n'est pas aussi efficace qu'un pli car la liste est toujours itérée plusieurs fois, mais elle est collectée en morceaux afin que les anciennes données puissent être efficacement récupérées. Pour briser cette propriété, il est nécessaire de conserver explicitement l'intégralité de 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 même un an. Nous apprenons quels combinateurs sont utiles (par exemple Zip
, breakE
, enumWith
) et lesquels le sont moins, avec pour résultat que les itératifs et combinateurs intégrés fournissent continuellement 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 chaque problème d'E/S.
J'utilise constamment des E/S paresseuses dans le code de production. C'est seulement un problème dans certaines circonstances, comme Don l'a mentionné. Mais pour la lecture de quelques fichiers, cela fonctionne très bien.
Mise à jour: Récemment sur haskell-cafe Oleg Kiseljov a montré que unsafeInterleaveST
(qui est utilisé pour implémenter lazy = IO dans la monade ST) est très dangereux - il casse 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 les IO paresseux: l'opération réelle IO peut être différée jusqu'à ce qu'il soit trop tard, par exemple après la fermeture du fichier. Citant de Haskell Wiki - Problèmes avec les IO paresseux =:
Par exemple, une erreur courante pour les 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 de forcer fileData. 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.
Ceci est souvent inattendu et une erreur facile à faire.
Voir aussi: Trois exemples de problèmes avec les E/S paresseuses .
Un autre problème avec lazy IO 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édire quand chaque partie de votre programme est évaluée , mais heureusement, en raison de la pureté, cela n'a vraiment aucune importance sauf si vous avez des problèmes de performances. Lorsque lazy IO est introduit, l'ordre d'évaluation de votre code a en fait un effet sur sa signification, vous êtes habitué à penser qu'inoffensif peut vous causer de vrais problèmes.
À titre d'exemple, voici une question sur le code qui semble raisonnable, mais qui est rendu plus confus 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 penser, et un mal de tête suffisamment grave pour que j'évite personnellement les paresseux IO sauf s'il y a un vrai problème avec tout le travail à l'avance.