Sur le wiki haskell, il y a l'exemple suivant d'utilisation conditionnelle de la monade IO (voir ici) .
when :: Bool -> IO () -> IO ()
when condition action world =
if condition
then action world
else ((), world)
Notez que dans cet exemple, la définition de IO a
est prise RealWorld -> (a, RealWorld)
pour rendre tout plus compréhensible.
Cet extrait exécute conditionnellement une action dans la monade IO. Maintenant, en supposant que condition
c'est le cas False
, l'action action
ne doit jamais être exécutée. En utilisant la sémantique paresseuse, ce serait effectivement le cas. Cependant, il est noté ici que Haskell est techniquement parlant non strict. Cela signifie que le compilateur est autorisé, par exemple, à s'exécuter de manière préventive action world
sur un autre thread, puis à supprimer ce calcul lorsqu'il découvre qu'il n'en a pas besoin. Cependant, à ce stade, les effets secondaires auront déjà eu lieu.
Maintenant, on pourrait implémenter la monade IO de telle manière que les effets secondaires ne se propagent que lorsque le programme entier est terminé, et nous savons exactement quels effets secondaires doivent être exécutés. Ce n'est pas le cas, cependant, car il est possible d'écrire des programmes infinis en Haskell, qui ont clairement des effets secondaires intermédiaires.
Est-ce à dire que la monade IO est techniquement erronée, ou y a-t-il autre chose qui empêche que cela se produise?
when
est typable, mais elle n'a pas le type que vous déclarez, et je ne vois pas ce qui rend ce code particulier intéressant.IO a
est définie commeRealWorld -> (a, RealWorld)
, afin de rendre les internes de IO plus lisibles.Réponses:
Il s'agit d'une "interprétation" suggérée de la
IO
monade. Si vous voulez prendre cette "interprétation" au sérieux, vous devez prendre au sérieux "RealWorld".action world
Peu importe qu'il soit évalué spéculativement ou non,action
n'a pas d'effets secondaires, ses effets, le cas échéant, sont gérés en renvoyant un nouvel état de l'univers où ces effets se sont produits, par exemple, un paquet réseau a été envoyé. Cependant, le résultat de la fonction est((),world)
et donc le nouvel état de l'univers estworld
. Nous n'utilisons pas le nouvel univers que nous pouvons avoir évalué spéculativement sur le côté. L'état de l'univers estworld
.Vous avez probablement du mal à prendre cela au sérieux. Il y a de nombreuses façons, c'est au mieux superficiellement paradoxal et absurde. La concurrence est particulièrement non évidente ou folle dans cette perspective.
"Attendez, attendez", dites-vous. "
RealWorld
est juste un 'jeton'. Ce n'est pas réellement l'état de l'univers entier." D'accord, alors cette "interprétation" n'explique rien. Néanmoins, en tant que détail d'implémentation , c'est ainsi que les modèles GHCIO
. 1 Cependant, cela signifie que nous avons des «fonctions» magiques qui ont effectivement des effets secondaires et ce modèle ne donne aucune indication sur leur signification. Et, puisque ces fonctions ont en fait des effets secondaires, la préoccupation que vous soulevez est tout à fait pertinente. GHC doit faire tout son possible pour s'en assurerRealWorld
et ces fonctions spéciales ne sont pas optimisées de manière à changer le comportement prévu du programme.Personnellement (comme cela est probablement évident maintenant), je pense que ce modèle de "passage du monde"
IO
est tout simplement inutile et déroutant en tant qu'outil pédagogique. (Que ce soit utile pour la mise en œuvre, je ne sais pas. Pour GHC, je pense que c'est plus un artefact historique.)Une autre approche consiste à considérer
IO
comme une description des demandes avec des gestionnaires de réponse. Il existe plusieurs façons de procéder. Le plus accessible est probablement d'utiliser une construction de monade gratuite, en particulier nous pouvons utiliser:Il existe de nombreuses façons de le rendre plus sophistiqué et d'avoir des propriétés un peu meilleures, mais c'est déjà une amélioration. Il ne nécessite pas d'hypothèses philosophiques profondes sur la nature de la réalité pour comprendre. Tout ce qu'il dit, c'est que
IO
c'est soit un programme trivialReturn
qui ne fait que renvoyer une valeur, soit une demande au système d'exploitation avec un gestionnaire pour la réponse.OSRequest
peut être quelque chose comme:De même,
OSResponse
pourrait être quelque chose comme:(L'une des améliorations qui peuvent être apportées est de rendre les choses plus sécuritaires pour que vous sachiez que vous n'obtiendrez pas
OpenSucceeded
d'unePutStr
requête.) Ce modèleIO
décrit les requêtes qui sont interprétées par un système (pour la "vraie"IO
monade, c'est le runtime Haskell lui-même), puis, peut-être, ce système appellera le gestionnaire auquel nous avons fourni une réponse. Bien sûr, cela ne donne aucune indication sur la façon dont une demandePutStr "hello world"
doit être traitée, mais cela ne prétend pas non plus. Il indique clairement que cela est délégué à un autre système. Ce modèle est également assez précis. Tous les programmes utilisateur des systèmes d'exploitation modernes doivent demander au système d'exploitation de faire quoi que ce soit.Ce modèle fournit les bonnes intuitions. Par exemple, de nombreux débutants considèrent des choses comme l'
<-
opérateur comme "déballant"IO
, ou ont (malheureusement renforcé) des vues qu'unIO String
, disons, est un "conteneur" qui "contient" desString
(et<-
les sort ensuite). Cette vue demande-réponse rend cette perspective clairement fausse. Il n'y a aucun descripteur de fichier à l'intérieur deOpenFile "foo" (\r -> ...)
. Une analogie courante pour souligner cela est qu'il n'y a pas de gâteau à l'intérieur d'une recette de gâteau (ou peut-être que la "facture" serait mieux dans ce cas).Ce modèle fonctionne également facilement avec la concurrence. Nous pouvons facilement avoir un constructeur pour
OSRequest
likeFork :: (OSResponse -> IO ()) -> OSRequest
et ensuite le runtime peut entrelacer les requêtes produites par ce gestionnaire supplémentaire avec le gestionnaire normal comme bon lui semble. Avec une certaine intelligence, vous pouvez utiliser cela (ou des techniques connexes) pour modéliser des choses comme la concurrence plus directement plutôt que de simplement dire "nous faisons une demande au système d'exploitation et les choses se produisent." C'est ainsi que fonctionne laIOSpec
bibliothèque .1 Hugs a utilisé une implémentation basée sur la continuation
IO
qui est à peu près similaire à ce que je décris bien qu'avec des fonctions opaques au lieu d'un type de données explicite. HBC a également utilisé une implémentation basée sur la continuation superposée à l'ancienne E / S basée sur le flux de demande-réponse. NHC (et donc YHC) utilisait des thunks, c'est-à-dire à peu prèsIO a = () -> a
si le()
était appeléWorld
, mais il ne faisait pas de passage d'état. JHC et UHC ont utilisé essentiellement la même approche que GHC.la source
OpenFile "foo" (\r -> ...)
devrait être le casRequest (OpenFile "foo") (\r -> ...)
?Request
. Pour répondre à votre première question, celaIO
est clairement insensible à l'ordre d'évaluation (fonds modulo) car c'est une valeur inerte. Tous les effets secondaires (le cas échéant) seraient causés par la chose qui interprète cette valeur. Dans l'when
exemple, cela n'aurait pas d'importance s'ilaction
était évalué, car ce serait juste une valeur comme celleRequest (PutStr "foo") (...)
que nous ne donnerons pas à la chose qui interprète ces demandes de toute façon. C'est comme du code source; peu importe que vous le réduisiez avec impatience ou paresseusement, rien ne se passe jusqu'à ce qu'il soit remis à un interprète.Request
pour commencer à voir les effets secondaires. Des effets secondaires ultérieurs peuvent être créés lors de l'évaluation de la poursuite. Intelligent!