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
IOmonade) 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 IOmonade, 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 -> bet g :: b -> cnous obtenons g . f :: a -> c. Notez que cela fonctionne aussi pour nos valeurs converties: si nous l'avons x :: aet 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 à .: fest égal à la fois à f . idet à 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 mde 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, mest [], 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 bet g :: b -> m cen 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 <=< returnsoit la même chose que fla même chose que return <=< f.
Tout ce m :: * -> *pour quoi nous avons de telles fonctions returnet <=<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 aet 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 returnet d' <=<avoir les propriétés que nous recherchons: associativité et identité gauche / droite. Exprimé en utilisant returnet >>=ils sont appelés les lois de la monade .
Un exemple - listes
Si nous choisissons md'ê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 IOou State), mais d'autres non, comme []ou Maybe.
La monade IO
Comme je l'ai mentionné, la IOmonade est quelque peu spéciale. Une valeur de type IO asignifie une valeur de type aconstruite 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 autilisant une construction pure. Voici IOsimplement 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 IOmonade:
- Composition
f :: a -> IO bet g :: b -> IO cmoyens: calcul fqui interagit avec l'environnement, puis calcul gqui utilise la valeur et calcule le résultat en interaction avec l'environnement.
returnajoute 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 IOmonade. 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
IOmonade. C'est pourquoi IOest 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
getChardoivent avoir un type de résultat IO something.