Les vues # 1 et # 2 sont incorrectes en général.
- N'importe quel type de données
* -> *
peut fonctionner comme une étiquette, les monades sont bien plus que cela.
- (À l'exception de la
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 valeur x :: 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 avons f :: a -> b
et g :: b -> c
nous obtenons g . f :: a -> c
. Notez que cela fonctionne aussi pour nos valeurs converties: si nous l'avons x :: a
et le convertissons en notre représentation, nous obtenons f . ((\_ -> x) :: () -> a) :: () -> b
.
Cette représentation a des propriétés très importantes, à savoir:
- Nous avons une fonction très spéciale - la fonction d' identité
id :: a -> a
pour chaque type a
. C'est un élément d'identité par rapport à .
: f
est égal à la fois à f . id
et à id . f
.
- L'opérateur de composition de fonction
.
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 type m :: * -> *
. (Dans Haskell, m
est []
, IO
, Maybe
, etc.) et la catégorie volonté contient toutes les fonctions de types a -> 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:
- Pour avoir un opérateur (appelons-le
<=<
) qui compose des fonctions f :: a -> m b
et g :: b -> m c
en quelque chose comme g <=< f :: a -> m c
. Et, il doit être associatif.
- Pour avoir une fonction d'identité pour chaque type, appelons-la
return
. Nous voulons également que ce f <=< return
soit la même chose que f
la même chose que return <=< f
.
Tout ce m :: * -> *
pour quoi nous avons de telles fonctions return
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 par m
.
(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 types m a
et nous composons des fonctions de types a -> 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 utilisant return
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 types a -> [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 de f :: a -> [b]
et g :: b -> [c]
fonctionne comme suit: g <=< f :: a -> [c]
signifie calculer tous les résultats possibles de type [b]
, appliquer g
à chacun d'eux et rassembler tous les résultats dans une seule liste. Exprimé à Haskell
return :: a -> [a]
return x = [x]
(<=<) :: (b -> [c]) -> (a -> [b]) -> (a -> [c])
g (<=<) f = concat . map g . f
ou en utilisant >>=
(>>=) :: [a] -> (a -> [b]) -> [b]
x >>= f = concat (map f x)
Notez que dans cet exemple, les types de retour étaient [a]
donc il était possible qu'ils ne contiennent aucune valeur de type a
. En effet, pour une monade, le type de retour ne doit pas avoir de telles valeurs. Certaines monades ont toujours (comme IO
ou State
), mais d'autres non, comme []
ou Maybe
.
La monade IO
Comme je l'ai mentionné, la IO
monade est quelque peu spéciale. Une valeur de type IO a
signifie une valeur de type a
construite en interagissant avec l'environnement du programme. Donc (contrairement à toutes les autres monades), nous ne pouvons pas décrire une valeur de type en IO a
utilisant une construction pure. Voici IO
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:
- Composition
f :: a -> IO b
et g :: b -> IO c
moyens: calcul f
qui interagit avec l'environnement, puis calcul g
qui utilise la valeur et calcule le résultat en interaction avec l'environnement.
return
ajoute simplement le IO
"tag" à la valeur (nous "calculons" simplement le résultat en gardant l'environnement intact).
- Les lois de monade (associativité, identité) sont garanties par le compilateur.
Quelques notes:
- Puisque les calculs monadiques ont toujours le type de résultat de
m a
, il n'y a aucun moyen de "s'échapper" de la IO
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.
- Lorsqu'un programmeur fonctionnel ne sait pas comment faire quelque chose de manière pure, il peut (en dernier recours) programmer la tâche par un calcul avec état dans la
IO
monade. C'est pourquoi IO
est souvent appelé le bac à péché d' un programmeur .
- Notez que dans un monde impur (au sens de la programmation fonctionnelle), la lecture d'une valeur peut également changer l'environnement (comme consommer les entrées de l'utilisateur). C'est pourquoi les fonctions comme
getChar
doivent avoir un type de résultat IO something
.