La monade IO est-elle techniquement incorrecte?

12

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 aest 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 conditionc'est le cas False, l'action actionne 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 worldsur 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?

Lasse
la source
Bienvenue en informatique ! Votre question est ici hors-sujet: nous traitons l' ordinateur la science des questions, pas des questions de programmation (voir notre FAQ ). Votre question pourrait être sur le sujet sur Stack Overflow .
dkaeae
2
À mon avis, c'est une question d'informatique, car elle traite de la sémantique théorique de Haskell, pas d'une question de programmation pratique.
Lasse
4
Je ne connais pas trop la théorie du langage de programmation, mais je pense que cette question est sur le sujet ici. Il pourrait être utile de clarifier ce que «mauvais» signifie ici. Quelle propriété pensez-vous que la monade IO possède qu'elle ne devrait pas avoir?
Lézard discret
1
Ce programme n'est pas bien typé. Je ne sais pas ce que vous vouliez vraiment écrire. La définition de whenest 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.
Gilles 'SO- arrête d'être méchant'
2
Ce programme est extrait mot pour mot de la page Haskell-wiki directement liée ci-dessus. Il ne tape en effet pas. En effet, il est écrit sous l'hypothèse qui IO aest définie comme RealWorld -> (a, RealWorld), afin de rendre les internes de IO plus lisibles.
Lasse

Réponses:

12

Il s'agit d'une "interprétation" suggérée de la IOmonade. Si vous voulez prendre cette "interprétation" au sérieux, vous devez prendre au sérieux "RealWorld". action worldPeu importe qu'il soit évalué spéculativement ou non, actionn'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 est world. Nous n'utilisons pas le nouvel univers que nous pouvons avoir évalué spéculativement sur le côté. L'état de l'univers est world.

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. " RealWorldest 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 GHC IO. 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 assurer RealWorldet 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" IOest 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 IOcomme 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:

data IO a = Return a | Request OSRequest (OSResponse -> IO a)

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 IOc'est soit un programme trivial Returnqui ne fait que renvoyer une valeur, soit une demande au système d'exploitation avec un gestionnaire pour la réponse. OSRequestpeut être quelque chose comme:

data OSRequest = OpenFile FilePath | PutStr String | ...

De même, OSResponsepourrait être quelque chose comme:

data OSResponse = Errno Int | OpenSucceeded Handle | ...

(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 OpenSucceededd'une PutStrrequête.) Ce modèle IOdécrit les requêtes qui sont interprétées par un système (pour la "vraie" IOmonade, 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 demande PutStr "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'un IO String, disons, est un "conteneur" qui "contient" des String(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 de OpenFile "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 OSRequestlike Fork :: (OSResponse -> IO ()) -> OSRequestet 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 la IOSpecbibliothèque .

1 Hugs a utilisé une implémentation basée sur la continuation IOqui 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ès IO a = () -> asi 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.

Derek Elkins a quitté SE
la source
Merci pour votre réponse éclairante, cela a vraiment aidé. Votre implémentation d'IO a pris un certain temps pour me faire une idée, mais je conviens qu'elle est plus intuitive. Êtes-vous en train de prétendre que cette implémentation ne souffre pas de problèmes potentiels avec la commande d'effets secondaires comme l'implémentation de RealWorld? Je ne vois pas immédiatement de problèmes, mais il n'est pas clair pour moi qu'ils n'existent pas.
Lasse
Un commentaire: il semble que ça OpenFile "foo" (\r -> ...)devrait être le cas Request (OpenFile "foo") (\r -> ...)?
Lasse
@Lasse Yep, ça aurait dû être avec Request. Pour répondre à votre première question, cela IOest 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' whenexemple, cela n'aurait pas d'importance s'il actionétait évalué, car ce serait juste une valeur comme celle Request (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.
Derek Elkins a quitté le SE
Ah oui je vois ça. Ceci est une définition vraiment intelligente. Au début, je pensais que tous les effets secondaires devraient nécessairement se produire lorsque l'ensemble du programme a terminé son exécution, car vous devez créer la structure de données avant de pouvoir l'interpréter. Mais comme une demande contient une continuation, il suffit de construire les données de la toute première Requestpour commencer à voir les effets secondaires. Des effets secondaires ultérieurs peuvent être créés lors de l'évaluation de la poursuite. Intelligent!
Lasse