web-dev-qa-db-fra.com

Pourquoi les effets secondaires sont-ils modélisés comme des monades dans Haskell?

Quelqu'un pourrait-il indiquer pourquoi les calculs impurs dans Haskell sont modélisés comme des monades?

Je veux dire que la monade n'est qu'une interface avec 4 opérations, alors quel était le raisonnement pour modéliser les effets secondaires?

164
bodacydo

Supposons qu'une fonction ait des effets secondaires. Si nous prenons tous les effets qu'il produit comme paramètres d'entrée et de sortie, alors la fonction est pure pour le monde extérieur.

Donc pour une fonction impure

f' :: Int -> Int

nous ajoutons le RealWorld à la considération

f :: Int -> RealWorld -> (Int, RealWorld)
-- input some states of the whole world,
-- modify the whole world because of the side effects,
-- then return the new world.

alors f est de nouveau pur. Nous définissons un type de données paramétré IO a = RealWorld -> (a, RealWorld), nous n'avons donc pas besoin de taper RealWorld autant de fois

f :: Int -> IO Int

Pour le programmeur, manipuler un RealWorld directement est trop dangereux - en particulier, si un programmeur met la main sur une valeur de type RealWorld, il pourrait essayer de copier ce qui est fondamentalement impossible. (Pensez à essayer de copier l'intégralité du système de fichiers, par exemple. Où le mettriez-vous?) Par conséquent, notre définition de IO encapsule également les états du monde entier.

Ces fonctions impures sont inutiles si nous ne pouvons pas les enchaîner. Considérer

getLine :: IO String               = RealWorld -> (String, RealWorld)
getContents :: String -> IO String = String -> RealWorld -> (String, RealWorld)
putStrLn :: String -> IO ()        = String -> RealWorld -> ((), RealWorld)

Nous voulons obtenir un nom de fichier à partir de la console, lire ce fichier, puis imprimer le contenu. Comment le ferions-nous si nous pouvions accéder aux états du monde réel?

printFile :: RealWorld -> ((), RealWorld)
printFile world0 = let (filename, world1) = getLine world0
                       (contents, world2) = (getContents filename) world1 
                   in  (putStrLn contents) world2 -- results in ((), world3)

Nous voyons un modèle ici: les fonctions sont appelées comme ceci:

...
(<result-of-f>, worldY) = f worldX
(<result-of-g>, worldZ) = g <result-of-f> worldY
...

Nous pourrions donc définir un opérateur ~~~ Pour les lier:

(~~~) :: (IO b) -> (b -> IO c) -> IO c

(~~~) ::      (RealWorld -> (b, RealWorld))
      -> (b -> RealWorld -> (c, RealWorld))
      ->       RealWorld -> (c, RealWorld)
(f ~~~ g) worldX = let (resF, worldY) = f worldX in
                        g resF worldY

alors nous pourrions simplement écrire

printFile = getLine ~~~ getContents ~~~ putStrLn

sans toucher au monde réel.


Supposons maintenant que nous voulions également mettre le contenu du fichier en majuscule. La majuscule est une fonction pure

upperCase :: String -> String

Mais pour entrer dans le monde réel, il doit retourner un IO String. Il est facile de lever une telle fonction:

impureUpperCase :: String -> RealWorld -> (String, RealWorld)
impureUpperCase str world = (upperCase str, world)

cela peut être généralisé:

impurify :: a -> IO a

impurify :: a -> RealWorld -> (a, RealWorld)
impurify a world = (a, world)

de sorte que impureUpperCase = impurify . upperCase, et nous pouvons écrire

printUpperCaseFile = 
    getLine ~~~ getContents ~~~ (impurify . upperCase) ~~~ putStrLn

(Remarque: Normalement, nous écrivons getLine ~~~ getContents ~~~ (putStrLn . upperCase))


Voyons maintenant ce que nous avons fait:

  1. Nous avons défini un opérateur (~~~) :: IO b -> (b -> IO c) -> IO c Qui enchaîne deux fonctions impures
  2. Nous avons défini une fonction impurify :: a -> IO a Qui convertit une valeur pure en impur.

Maintenant, nous faisons l'identification (>>=) = (~~~) Et return = impurify, Et voyez? Nous avons une monade.


(Pour vérifier s'il s'agit bien d'une monade, il y a peu d'axiomes à satisfaire:

(1) return a >>= f = f a

  impurify a               = (\world -> (a, world))
 (impurify a ~~~ f) worldX = let (resF, worldY) = (\world -> (a, world)) worldX 
                             in f resF worldY
                           = let (resF, worldY) =            (a, worldX))       
                             in f resF worldY
                           = f a worldX

(2) f >>= return = f

  (f ~~~ impurify) a worldX = let (resF, worldY) = impuify a worldX 
                              in f resF worldY
                            = let (resF, worldY) = (a, worldX)     
                              in f resF worldY
                            = f a worldX

(3) f >>= (\x -> g x >>= h) = (f >>= g) >>= h

Exercice.)

286
kennytm

Quelqu'un pourrait-il nous expliquer pourquoi les calculs impurs dans Haskell sont modélisés comme des monades?

Cette question contient un malentendu généralisé. Impureté et Monade sont des notions indépendantes. L'impureté n'est pas pas modélisée par Monad. Il existe plutôt quelques types de données, tels que IO, qui représentent un calcul impératif. Et pour certains de ces types, une infime fraction de leur interface correspond au modèle d'interface appelé "Monade". De plus, il n'existe aucune explication pure/fonctionnelle/dénotative connue de IO (et il est peu probable qu'il y en ait une, compte tenu de la fonction "sin bin" de IO) , bien qu'il y ait l'histoire souvent racontée à propos de World -> (a, World) étant le sens de IO a. Cette histoire ne peut pas vraiment décrire IO, car IO prend en charge la concurrence et le non-déterminisme. L'histoire ne fonctionne même pas quand il s'agit de calculs déterministes qui permettent une interaction à mi-calcul avec le monde.

Pour plus d'explications, voir cette réponse .

Edit : En relisant la question, je ne pense pas que ma réponse soit tout à fait sur la bonne voie. Les modèles de calcul impératif se révèlent souvent être des monades, comme l'a dit la question. Le demandeur pourrait ne pas vraiment supposer que la monadness permet en aucune façon la modélisation du calcul impératif.

43
Conal

Si je comprends bien, quelqu'un appelé Eugenio Moggi a d'abord remarqué qu'une construction mathématique auparavant obscure appelée "monade" pouvait être utilisée pour modéliser les effets secondaires dans les langages informatiques, et donc spécifier leur sémantique en utilisant le calcul Lambda. Lors du développement de Haskell, il y avait différentes manières de modéliser les calculs impurs (voir le document de Simon Peyton Jones "chemise de cheveux" pour plus de détails), mais lorsque Phil Wadler a introduit les monades, il est rapidement devenu évident que cette était la réponse. Et le reste est de l'histoire.

13
Paul Johnson

Quelqu'un pourrait-il nous expliquer pourquoi les calculs impurs dans Haskell sont modélisés comme des monades?

Eh bien, parce que Haskell est pur . Vous avez besoin d'un concept mathématique pour distinguer entre calculs impurs et purs on niveau-type et pour modéliser flux de programme dans respectivement.

Cela signifie que vous devrez vous retrouver avec un type IO a qui modélise un calcul impur. Ensuite, vous devez savoir comment combiner ces calculs dont appliquer en séquence (>>=) et soulevez une valeur (return) sont les plus évidents et les plus basiques.

Avec ces deux, vous avez déjà défini une monade (sans même y penser);)

De plus, les monades fournissent des abstractions très générales et puissantes, de nombreux types de flux de contrôle peuvent être facilement généralisés dans des fonctions monadiques comme sequence, liftM ou une syntaxe spéciale , faisant de l'impureté pas un cas si spécial.

Voir monades en programmation fonctionnelle et typage d'unicité (la seule alternative que je connaisse) pour plus d'informations.

8
Dario

Comme vous le dites, Monad est une structure très simple. La moitié de la réponse est: Monad est la structure la plus simple que nous pourrions éventuellement donner aux fonctions à effets secondaires et pouvoir les utiliser. Avec Monad, nous pouvons faire deux choses: nous pouvons traiter une valeur pure comme une valeur d'effet secondaire (return), et nous pouvons appliquer une fonction d'effet secondaire à une valeur d'effet secondaire à obtenir une nouvelle valeur d'effet secondaire (>>=). Perdre la capacité de faire l'une de ces choses serait paralysant, donc notre type d'effet secondaire doit être "au moins" Monad, et il s'avère que Monad est suffisant pour implémenter tout ce que nous ' ve nécessaire jusqu'à présent.

L'autre moitié est: quelle est la structure la plus détaillée que nous pourrions donner aux "effets secondaires possibles"? Nous pouvons certainement penser à l'espace de tous les effets secondaires possibles comme un ensemble (la seule opération qui nécessite est l'appartenance). Nous pouvons combiner deux effets secondaires en les faisant l'un après l'autre, et cela donnera lieu à un effet secondaire différent (ou peut-être le même - si le premier était "éteindre l'ordinateur" et le second était "écrire un fichier", alors le résultat de les composer est simplement un "ordinateur éteint").

Ok, que dire de cette opération? C'est associatif; c'est-à-dire que si nous combinons trois effets secondaires, peu importe l'ordre dans lequel nous faisons la combinaison. Si nous faisons (écrire le fichier puis lire le socket) puis éteindre l'ordinateur, c'est la même chose que faire le fichier d'écriture alors (lire le socket puis arrêter ordinateur). Mais ce n'est pas commutatif: ("écrire le fichier" puis "supprimer le fichier") est un effet secondaire différent de ("supprimer le fichier" puis "écrire le fichier"). Et nous avons une identité: l'effet secondaire spécial "pas d'effets secondaires" fonctionne ("pas d'effets secondaires" puis "supprimer le fichier" est le même effet secondaire que simplement "supprimer le fichier") À ce stade, tout mathématicien pense "Groupe!" Mais les groupes ont des inverses, et il n'y a aucun moyen d'inverser un effet secondaire en général; "supprimer le fichier" est irréversible. La structure qui nous reste est donc celle d'un monoïde, ce qui signifie que nos fonctions d'effet secondaire devraient être des monades.

Existe-t-il une structure plus complexe? Sûr! Nous pourrions diviser les effets secondaires possibles en effets basés sur le système de fichiers, effets basés sur le réseau et plus, et nous pourrions trouver des règles de composition plus élaborées qui préservent ces détails. Mais encore une fois, cela revient à: Monad est très simple, et pourtant assez puissant pour exprimer la plupart des propriétés qui nous intéressent. (En particulier, l'associativité et les autres axiomes nous permettent de tester notre application en petits morceaux, avec la certitude que les effets secondaires de l'application combinée seront les mêmes que la combinaison des effets secondaires des morceaux).

6
lmm

C'est en fait une façon assez claire de penser les E/S de manière fonctionnelle.

Dans la plupart des langages de programmation, vous effectuez des opérations d'entrée/sortie. Dans Haskell, imaginez écrire du code non pas pour faire les opérations, mais pour générer une liste des opérations que vous souhaitez effectuer.

Les monades sont juste une jolie syntaxe pour cela.

Si vous voulez savoir pourquoi les monades par opposition à autre chose, je suppose que la réponse est qu'elles sont la meilleure façon fonctionnelle de représenter les E/S auxquelles les gens pouvaient penser lorsqu'ils fabriquaient Haskell.

4
Noah Lavine

AFAIK, la raison est de pouvoir inclure des contrôles des effets secondaires dans le système de typage. Si vous voulez en savoir plus, écoutez ces SE-Radio épisodes: Episode 108: Simon Peyton Jones sur la programmation fonctionnelle et Haskell Episode 72: Erik Meijer sur LINQ

3
Gabriel Ščerbák

Ci-dessus, il y a de très bonnes réponses détaillées avec un fond théorique. Mais je veux donner mon avis sur IO monad. Je ne suis pas un programmeur haskell expérimenté, donc c'est peut-être assez naïf ou même faux. Mais je m'a aidé à gérer IO monade dans une certaine mesure (notez que cela ne concerne pas les autres monades).

Je veux d'abord dire que cet exemple avec le "monde réel" n'est pas trop clair pour moi car nous ne pouvons pas accéder à ses états précédents (du monde réel). Peut-être que cela ne concerne pas du tout les calculs monades, mais cela est souhaité dans le sens de la transparence référentielle, qui est généralement présente dans le code haskell.

Nous voulons donc que notre langue (haskell) soit pure. Mais nous avons besoin d'opérations d'entrée/sortie car sans elles notre programme ne peut pas être utile. Et ces opérations ne peuvent être pures de par leur nature. Donc, la seule façon de gérer cela est de séparer les opérations impures du reste du code.

Voici monade vient. En fait, je ne suis pas sûr, qu'il ne puisse exister d'autre construction avec des propriétés nécessaires similaires, mais le fait est que la monade a ces propriétés, donc elle peut être utilisée (et elle est utilisée avec succès). La propriété principale est que nous ne pouvons pas y échapper. L'interface de la monade n'a pas d'opérations pour se débarrasser de la monade autour de notre valeur. D'autres monades (non IO) fournissent de telles opérations et permettent la correspondance de modèles (par exemple, peut-être), mais ces opérations ne sont pas dans l'interface de monade. Une autre propriété requise est la capacité de chaîner les opérations.

Si nous pensons à ce dont nous avons besoin en termes de système de type, nous arrivons au fait que nous avons besoin de type avec constructeur, qui peut être enroulé autour de n'importe quelle vallée. Le constructeur doit être privé, car nous interdisons d'y échapper (c'est-à-dire la correspondance de motifs). Mais nous avons besoin de fonction pour mettre de la valeur dans ce constructeur (ici, le retour me vient à l'esprit). Et nous avons besoin du moyen d'enchaîner les opérations. Si nous y réfléchissons pendant un certain temps, nous arriverons au fait que l'opération de chaînage doit avoir le type comme >> = a. Donc, nous arrivons à quelque chose de très similaire à la monade. Je pense que si nous analysons maintenant d'éventuelles situations contradictoires avec cette construction, nous arriverons à des axiomes monades.

Notez que cette construction développée n'a rien de commun avec l'impureté. Il n'a que des propriétés que nous souhaitions avoir pour pouvoir faire face à des opérations impures, à savoir le non-échappement, le chaînage et un moyen d'entrer.

Maintenant, un ensemble d'opérations impures est prédéfini par le langage dans cette IO monade sélectionnée. Nous pouvons combiner ces opérations pour créer de nouvelles opérations impures. Et toutes ces opérations devront avoir IO dans leur type. Notez cependant que la présence de IO dans le type de certaines fonctions ne rend pas cette fonction impure). Mais si je comprends bien, c'est une mauvaise idée d'écrire des fonctions pures avec IO dans leur type, car c'était initialement notre idée de séparer les fonctions pures et impures.

Enfin, je tiens à dire que la monade ne transforme pas les opérations impures en opérations pures. Il permet seulement de les séparer efficacement. (Je répète, c'est seulement ma compréhension)

2
Dmitrii Semikin