Tout en apprenant Haskell, j'ai fait face à de nombreux tutoriels essayant d'expliquer ce que sont les monades et pourquoi les monades sont importantes dans Haskell. Chacun d'eux a utilisé des analogies, il serait donc plus facile de saisir le sens. À la fin de la journée, je me retrouve avec 3 vues différentes de ce qu'est une monade:
Vue 1: Monade comme étiquette
Parfois, je pense qu'une monade comme une étiquette à un type spécifique. Par exemple, une fonction de type:
myfunction :: IO Int
myfunction est une fonction qui, chaque fois qu'elle est effectuée, produira une valeur Int. Le type du résultat n'est pas Int mais IO Int. Ainsi, IO est une étiquette de la valeur Int avertissant l'utilisateur de savoir que la valeur Int est le résultat d'un processus où une action IO a été effectuée.
Par conséquent, cette valeur Int a été marquée comme valeur provenant d'un processus avec IO, par conséquent, cette valeur est "sale". Votre processus n'est plus pur.
Vue 2: Monade comme un espace privé où des choses désagréables peuvent se produire.
Dans un système où tous les processus sont purs et stricts, vous devez parfois avoir des effets secondaires. Ainsi, une monade n'est qu'un petit espace qui vous permet de faire des effets secondaires désagréables. Dans cet espace, vous êtes autorisé à échapper au monde pur, à devenir impur, à faire votre processus et à revenir avec une valeur.
Vue 3: Monade comme dans la théorie des catégories
C'est le point de vue que je ne comprends pas complètement. Une monade n'est qu'un foncteur de la même catégorie ou d'une sous-catégorie. Par exemple, vous avez les valeurs Int et en tant que sous-catégorie IO Int, qui sont les valeurs Int générées après un processus IO.
Ces vues sont-elles correctes? Quel est le plus précis?
Réponses:
Les vues # 1 et # 2 sont incorrectes en général.
* -> *
peut fonctionner comme une étiquette, les monades sont bien plus que cela.IO
monade) les calculs au sein d'une monade ne sont pas impurs. Ils représentent simplement des calculs que nous percevons comme ayant des effets secondaires, mais ils sont purs.Ces deux malentendus proviennent de la concentration sur la
IO
monade, qui est en fait un peu spéciale.J'essaierai de développer un peu le # 3, sans entrer dans la théorie des catégories si possible.
Calculs standard
Tous les calculs dans un langage de programmation fonctionnelle peuvent être considérés comme des fonctions avec un type de source et un type de cible:
f :: a -> b
. Si une fonction a plus d'un argument, nous pouvons la convertir en fonction à un argument par curry (voir aussi le wiki Haskell ). Et si nous avons juste une valeurx :: a
(fonction avec 0 arguments), on peut le convertir en une fonction qui prend un argument du type d'unité :(\_ -> x) :: () -> a
.Nous pouvons construire des programmes plus complexes à partir de programmes plus simples en composant ces fonctions à l'aide de l'
.
opérateur. Par exemple, si nous avonsf :: a -> b
etg :: b -> c
nous obtenonsg . f :: a -> c
. Notez que cela fonctionne aussi pour nos valeurs converties: si nous l'avonsx :: a
et le convertissons en notre représentation, nous obtenonsf . ((\_ -> x) :: () -> a) :: () -> b
.Cette représentation a des propriétés très importantes, à savoir:
id :: a -> a
pour chaque typea
. C'est un élément d'identité par rapport à.
:f
est égal à la fois àf . id
et àid . f
..
est associatif .Calculs monadiques
Supposons que nous voulons sélectionner et travailler avec une catégorie spéciale de calculs, dont le résultat contient quelque chose de plus que la seule valeur de retour. Nous ne voulons pas spécifier ce que "quelque chose de plus" signifie, nous voulons garder les choses aussi générales que possible. La façon la plus générale de représenter «quelque chose de plus» est de la représenter comme une fonction de type - un type
m
de type* -> *
(c'est-à-dire qu'elle convertit un type en un autre). Donc, pour chaque catégorie de calculs avec laquelle nous voulons travailler, nous aurons une fonction de typem :: * -> *
. (Dans Haskell,m
est[]
,IO
,Maybe
, etc.) et la catégorie volonté contient toutes les fonctions de typesa -> m b
.Maintenant, nous aimerions travailler avec les fonctions d'une telle catégorie de la même manière que dans le cas de base. Nous voulons pouvoir composer ces fonctions, nous voulons que la composition soit associative, et nous voulons avoir une identité. Nous avons besoin:
<=<
) qui compose des fonctionsf :: a -> m b
etg :: b -> m c
en quelque chose commeg <=< f :: a -> m c
. Et, il doit être associatif.return
. Nous voulons également que cef <=< return
soit la même chose quef
la même chose quereturn <=< f
.Tout ce
m :: * -> *
pour quoi nous avons de telles fonctionsreturn
et<=<
est appelé une monade . Il nous permet de créer des calculs complexes à partir de calculs plus simples, tout comme dans le cas de base, mais maintenant les types de valeurs de retour sont transformés parm
.(En fait, j'ai un peu abusé du terme catégorie ici. Dans le sens de la théorie des catégories, nous ne pouvons appeler notre construction une catégorie qu'après avoir su qu'elle obéit à ces lois.)
Monades à Haskell
Dans Haskell (et dans d'autres langages fonctionnels), nous travaillons principalement avec des valeurs, pas avec des fonctions de types
() -> a
. Ainsi, au lieu de définir<=<
pour chaque monade, nous définissons une fonction(>>=) :: m a -> (a -> m b) -> m b
. Une telle définition alternative est équivalente, nous pouvons l'exprimer en>>=
utilisant<=<
et vice versa (essayez comme un exercice, ou voyez les sources ). Le principe est moins évident maintenant, mais il reste le même: nos résultats sont toujours de typesm a
et nous composons des fonctions de typesa -> m b
.Pour chaque monade que nous créons, nous ne devons pas oublier de vérifier cela
return
et d'<=<
avoir les propriétés que nous recherchons: associativité et identité gauche / droite. Exprimé en utilisantreturn
et>>=
ils sont appelés les lois de la monade .Un exemple - listes
Si nous choisissons
m
d'être[]
, nous obtenons une catégorie de fonctions de typesa -> [b]
. Ces fonctions représentent des calculs non déterministes, dont les résultats peuvent être une ou plusieurs valeurs, mais également aucune valeur. Cela donne naissance à la soi-disant monade de liste . La composition def :: a -> [b]
etg :: b -> [c]
fonctionne comme suit:g <=< f :: a -> [c]
signifie calculer tous les résultats possibles de type[b]
, appliquerg
à chacun d'eux et rassembler tous les résultats dans une seule liste. Exprimé à Haskellou en utilisant
>>=
Notez que dans cet exemple, les types de retour étaient
[a]
donc il était possible qu'ils ne contiennent aucune valeur de typea
. En effet, pour une monade, le type de retour ne doit pas avoir de telles valeurs. Certaines monades ont toujours (commeIO
ouState
), mais d'autres non, comme[]
ouMaybe
.La monade IO
Comme je l'ai mentionné, la
IO
monade est quelque peu spéciale. Une valeur de typeIO a
signifie une valeur de typea
construite en interagissant avec l'environnement du programme. Donc (contrairement à toutes les autres monades), nous ne pouvons pas décrire une valeur de type enIO a
utilisant une construction pure. VoiciIO
simplement une balise ou une étiquette qui distingue les calculs qui interagissent avec l'environnement. C'est (le seul cas) où les vues # 1 et # 2 sont correctes.Pour la
IO
monade:f :: a -> IO b
etg :: b -> IO c
moyens: calculf
qui interagit avec l'environnement, puis calculg
qui utilise la valeur et calcule le résultat en interaction avec l'environnement.return
ajoute simplement leIO
"tag" à la valeur (nous "calculons" simplement le résultat en gardant l'environnement intact).Quelques notes:
m a
, il n'y a aucun moyen de "s'échapper" de laIO
monade. La signification est la suivante: une fois qu'un calcul interagit avec l'environnement, vous ne pouvez pas construire à partir de lui un calcul qui ne fonctionne pas.IO
monade. C'est pourquoiIO
est souvent appelé le bac à péché d' un programmeur .getChar
doivent avoir un type de résultatIO something
.la source
IO
n'a pas de sémantique particulière du point de vue de la langue. Ce n'est pas spécial, il se comporte comme n'importe quel autre code. Seule l'implémentation de la bibliothèque d'exécution est spéciale. En outre, il existe un moyen spécial de s'échapper (unsafePerformIO
). Je pense que c'est important parce que les gens pensent souventIO
comme un élément de langage spécial ou une balise déclarative. Ce n'est pas.coerce :: a -> b
qui convertit deux types (et planter votre programme dans la plupart des cas). Voir cet exemple - vous pouvez même convertir une fonction enInt
etc.runST :: (forall s. GHC.ST.ST s a) -> a
Vue 1: Monade comme étiquette
"Par conséquent, cette valeur Int a été marquée comme valeur provenant d'un processus avec IO, cette valeur est donc" sale "."
"IO Int" n'est pas en général une valeur Int (bien qu'elle puisse être dans certains cas comme "return 3"). Il s'agit d'une procédure qui génère une valeur Int. Différentes exécutions de cette "procédure" peuvent donner des valeurs Int différentes.
Une monade m, est un "langage de programmation" embarqué (impératif): dans ce langage il est possible de définir quelques "procédures". Une valeur monadique (de type ma), est une procédure dans ce "langage de programmation" qui sort une valeur de type a.
Par exemple:
est une procédure qui génère une valeur de type Int.
Ensuite:
est une procédure qui produit deux Ints (éventuellement différents).
Chacune de ces "langues" prend en charge certaines opérations:
deux procédures (ma et mb) peuvent être "concaténées": vous pouvez créer une procédure plus grande (ma >> mb) composée de la première puis de la seconde;
de plus la sortie (a) de la première peut affecter la seconde (ma >> = \ a -> ...);
une procédure (return x) peut donner une valeur constante (x).
Les différents langages de programmation intégrés diffèrent sur les choses gentilles qu'ils prennent en charge, telles que:
la source
Ne confondez pas un type monadique avec la classe monade.
Un type monadique (c'est-à-dire un type étant une instance de la classe monade) résoudrait un problème particulier (en principe, chaque type monadique résout un problème différent): État, aléatoire, peut-être, IO. Tous sont des types avec contexte (ce que vous appelez "label", mais ce n'est pas ce qui fait d'eux une monade).
Pour chacun d'eux, il faut "enchaîner les opérations avec choix" (une opération dépend du résultat de la précédente). Ici intervient la classe monade: demandez à votre type (résoudre un problème donné) d'être une instance de la classe monade et le problème de chaînage est résolu.
Voir Que résout la classe monade?
la source