Comprendre les monades

C’est quoi ? À quoi ça sert ?

Publié par Dominus Carnufex le 11 juillet 2016 sous licence BiPu-L .

Les monades, ça fait peur. On en entend généralement parler quand on se lance dans l’étude du Haskell, dont elles sont souvent présentées comme la principale difficulté, voire de la programmation fonctionnelle de manière générale. Pourtant, si l’on se restreint à l’essentiel, à savoir ce que c’est, et quelle est leur utilité, c’est vraiment tout bête.

Alors voilà l’objectif que se fixe ce cours : faire comprendre simplement ce que sont les monades, et guère plus. On n’expliquera pas comment créer une monade dans les formes, on ne parlera pas des lois des monades, a fortiori des mathématiques derrière les monades, on ne parlera pas non plus des usages avancés des monades, comme les transformateurs de monades ou les monades additives.

Juste l’essentiel, l’indispensable. Qu’est-ce que c’est. Quelques exemples de cas où elles sont utiles. Quelques exemples de cas où elles ne sont pas utiles. Le reste sera traité dans d’autres cours.

Tous les codes sont expliqués : il n’est donc pas nécessaire de connaître le Haskell pour comprendre ce cours. Malgré tout, en connaître les principales syntaxes aide grandement. La troisième section montre des exemples de monades dans d’autres langages que le Haskell, mais le code est là aussi expliqué, et ne nécessite pas de connaître le langage employé.

En revanche, le cours considère comme acquis les concepts usuels de la programmation (fonction, variable, etc.) et sera difficilement1 accessible à quelqu’un qui n’aurait aucune pratique de celle-ci.

Définition et exemples

Une monade, c’est une boite, pouvant contenir un ou plusieurs objets de même nature, conçue de telle manière que l’on puisse placer un/des objet(s) dans la boite, et par ailleurs, sortir le contenu de la boite juste le temps d’effectuer une action sur lui, avant de le remettre dans la boite.

Notez bien que les objets sont de même nature à un instant donné : l’action effectuée sur les objets peut en changer la nature, du moment qu’elle change la nature de tous les objets. Pour prendre une comparaison concrète, on peut sortir des sacs de grain et remettre des sacs de farine.

Voilà pour l’approche intuitive. De manière un peu plus concrète, et avec du vocabulaire plus formel, une abstraction informatique peut être considérée comme une monade si elle réunit les éléments suivants.

Prenons le Haskell comme langage de travail, et essayons de créer la plus simple des monades. Il nous faut d’abord un type paramétré. Comme on veut créer la plus simple de toutes les monades, ce type paramétré ne pourra contenir qu’un seul objet.

data MonType a = MonConstructeur a

Pour ceux qui ne seraient pas familiers avec le langage, le mot-clé data signifie que l’on va définir un nouveau type. MonType est le nom du type, et le a signale que c’est un type paramétré à un seul paramètre. Ce qui vient après le = est un constructeur de valeur, là aussi à un seul paramètre : on lui fournit une valeur de n’importe quel type, et il l’emballe pour en faire un objet de type MonType.

Passons à la fonction return. Stricto sensu, le constructeur de valeur dont nous venons de parler fait déjà le travail, mais on va la définir quand même, juste pour être complets.

return :: a -> MonType a
return x = MonConstructeur x

La première ligne n’est pas entièrement nécessaire, elle se contente de dire que return prend un objet de n’importe quel type, et renvoie une valeur monadique de type MonType contenant ce même type. La seconde ligne dit strictement ce que nous avons fait remarquer trois lignes plus haut : return est parfaitement équivalent au constructeur de valeur.

Vient la partie la plus complexe, à savoir bind. En voici le code.

bind :: MonType a -> (a -> MonType b) -> MonType b
bind (MonConstructeur objet) fonction = fonction objet

Là encore, la première ligne n’est pas totalement nécessaire. Elle fixe les mêmes conditions de type que données plus haut dans l’explication formelle. Quant à la seconde, elle est un tout petit peu plus complexe, alors voyons les choses pas à pas.

La fonction bind prend deux paramètres, (MonConstructeur objet) et fonction. Le premier est rédigé de manière à utiliser le filtrage par motif, qui permet d’accéder de manière élégante à la valeur contenue dans l’emballage MonType. La partie après le = définit le fonctionnement de bind, à savoir simplement appliquer fonction au contenu de la valeur monadique.

Son utilisation est alors très simple.

multiplier_par_deux :: Int -> MonType Int
multiplier_par_deux x = return (x * 2)

valeur_monadique = return 4
nouvelle_valeur_monadique = bind valeur_monadique multiplier_par_deux

La fonction nouvelle_valeur_monadique renverra 8 encapsulé dans un objet de type MonType. Et voilà ! Vous avez devant vous votre première monade. Moins effrayant qu’il n’y paraît, n’est-ce pas ? Et parfaitement inutile, soyons honnêtes. Mais elle permet de mettre en lumière une vérité première sur les monades, qu’il faut toujours avoir à l’esprit quand on s’intéresse à elles.

Alors voyons quelques exemples et contre-exemples, toujours en Haskell.

Le type Maybe peut être défini comme suit, accompagné de ses fonctions return et bind.

data Maybe a = Nothing | Just a

return :: a -> Maybe a
return x = Just x

bind :: Maybe a -> (a -> Maybe b) -> Maybe b
bind (Just a) f = f a
bind Nothing _ = Nothing

La situation est très légèrement plus complexe, mais vous devriez reconnaître la structure générale. On commence par définir le type paramétré Maybe, qui peut prendre deux formes différentes : soit Nothing, qui n’encapsule rien, soit Just qui, lui, peut encapsuler n’importe quoi.

Comme pour MonType, la fonction return est parfaitement équivalente au constructeur de valeur Just, puisque celui-ci se contente de prendre une valeur, et de l’emballer dans un objet de type Maybe.

La fonction bind est plus nettement différente, mais vous remarquerez que la définition de son type suit strictement le même modèle que MonType. Que disent les deux lignes restantes ? Que si la valeur monadique est Nothing, c’est-à-dire n’encapsule rien, alors on ne peut pas appliquer une fonction sur une valeur qui n’existe pas, et on se contente de renvoyer à nouveau Nothing. Si au contraire la valeur monadique a la forme Just quelque_chose, alors on applique la fonction sur ce quelque chose, comme prévu.

Comme vous le voyez, le triplet Maybe a, return, bind respecte les conditions imposées, et constitue donc une monade. Quelle est son utilité ? Elle sert à gérer les opérations qui peuvent échouer. Mais nous reviendrons plus tard là-dessus.

Il existe un autre type dans la bibliothèque standard du Haskell, qui sert à gérer les opérations qui peuvent échouer, et il est défini comme suit.

data Either a b = Left a | Right b

On voit d’emblée que ce type ne peut pas servir de base à une monade. En effet, il possède deux paramètres, et non un seul, ce qui le disqualifie d’office. Vous retiendrez donc que c’est la forme, et non l’utilité, qui fait la monade.

On s’en convaincra en voyant cette autre monade, assez nettement différente de Maybe.

data Liste a = ListeVide | ListePasVide a (Liste a)
-- ListePasVide encapsule le premier élément de la liste,
--   puis le reste de la liste d’un bloc.

return :: a -> Liste a
return x = ListePasVide x ListeVide
-- Une liste à un seul élément contient donc un premier élément,
--   puis un « reste de la liste » qui est vide.

bind :: Liste a -> (a -> Liste b) -> Liste b
bind ListeVide _ = ListeVide
bind liste f = concat (map f liste)

Il ne s’agit pas du vrai type de liste du Haskell, car celui-ci utilise une syntaxe trop particulière pour rester compréhensible, mais le principe est le même. On a un type à un seul paramètre, qui est cette fois récursif : il encapsule, outre une valeur, un autre objet du même type que lui. En pratique, cela revient à encapsuler plusieurs objets du même type.

La fonction return se contente d’encapsuler la valeur fournie en entrée dans une liste dont elle est le seul élément. Quant à bind elle applique la fonction f sur chaque élément de la liste fournie en entrée, avant « d’écraser » les deux niveaux de liste en un seul. Cet exemple vous permettra d’y voir un peu plus clair.

ma_fonction :: Int -> [Int]
ma_fonction x = [x + 1, x + 10]
-- ``ma_fonction`` prend un entier et renvoie une liste composée de :
--  — cet entier plus 1 ;
--  — cet entier plus 10.

ma_liste :: [Int]
ma_liste = [0, 12, 54]
-- Une simple liste d’entiers.

bindons :: [Int]
bindons = bind ma_liste ma_fonction
-- Donne pour résultat ``[1, 10, 13, 22, 55, 64]``. Comment en arrive-t-on là ?
-- Pour commencer, ``map ma_fonction ma_liste`` applique ``ma_fonction`` à chaque élément
--   de ``ma_liste``. On obtient donc le résultat ``[[1, 10], [13, 22], [55, 64]]``.
-- Puis ``concat`` « écrase » les deux niveaux de liste en un seul, donnant le résultat
--   attendu. On a bien une liste d’entiers en entrée, et une liste d’entiers en sortie.

Le détail du fonctionnement importe peu. Ce qu’il faut retenir, c’est que la fonction bind respecte strictement les conditions imposées par le cahier des charges, même s’il lui faut biaiser un peu pour cela, et cela suffit à faire une monade du type liste accompagné de ces deux fonctions.

Utilité

Il n’y a qu’une seule réponse possible à cette question : cela dépend de la monade. Nous avons vu dans la section précédente qu’une monade pouvait tout simplement ne servir à rien. Mais supposons que nous ne conservions que les « bonnes » monades, celles qui sont utiles au programmeur ?

L’utilité la plus évidente est de simplifier le code utilisant le type qui sert de base à la monade. Revenons à notre type Maybe : on peut l’utiliser pour écrire une fonction de division un peu plus propre que celle de départ. Comparez en effet ces deux fonctions.

divMoche :: Double -> Double -> Double
divMoche x y = if y == 0
               then error "Division par 0 interdite !"
               else x / y

divM :: Double -> Double -> Maybe Double
divM x y = if y == 0 then Nothing else Just (x / y)

Voyez la très légère différence dans la déclaration de type de chaque fonction. À quoi cela a-t-il servi ? La fonction error abandonne l’exécution du programme et affiche une erreur. Si votre programme a pour seule utilité de faire une division, ce n’est pas très grave, mais si votre calculette plantait définitivement chaque fois que vous cherchez à faire une opération n’ayant pas de sens, cela deviendrait vite pénible.

Alors qu’avec divM, c’est le résultat lui-même qui signale qu’il y a eu une erreur. Supposons alors que nous voulions faire plusieurs divisions à la suite7. Il faut, à chaque division, vérifier que la précédente n’a pas abouti à une erreur. Cela donnerait le code qui suit, pour trois divisions successives.

divM_3 :: Double -> Double -> Double -> Double -> Maybe Double
divM_3 x y1 y2 y3 =
    case divM x y1 of
        Nothing -> Nothing
        Just x2 -> case divM x2 y2 of
            Nothing -> Nothing
            Just x3 -> divM x3 y3

Pour ceux qui ne connaîtraient pas la syntaxe, case <qqch> of compare la valeur de <qqch> aux valeurs ou motifs situés avant les flèches, et exécute le traitement correspondant situé après la flèche. C’est une forme étendue de if … then … else. Et pour ajouter une nouvelle division, il faudrait ajouter encore un niveau d’imbrication. Cela n’est évidemment pas optimal, et c’est là qu’intervient la monade.

Reprenons notre fonction divM de plus haut, ainsi que les fonctions return et bind que nous avions écrites dans la première section. Voici comment on pourrait écrire divM_3.

divM_3 :: Double -> Double -> Double -> Double -> Maybe Double
divM_3 x y1 y2 y3 = return x
                    `bind` \x1 -> divM x1 y1
                    `bind` \x2 -> divM x2 y2
                    `bind` \x3 -> divM x3 y3

Il y a quelques nouveaux éléments de syntaxe à expliciter. Le fait de mettre la fonction bind entre apostrophes inversées ` permet de placer son premier paramètre avant elle, et son second paramètre après, à la manière d’un opérateur (comme + ou /).

En outre, la structure \x1 -> divM x1 y1 est une lambda ou fonction anonyme : elle prend un seul paramètre, x1, et renvoie ce qui se trouve après la flèche. Cela permet de créer à peu de frais des fonctions « diviser par y1 », « diviser par y2 », etc. Lesquelles ne prenant qu’un seul paramètre, peuvent être bindées au résultat de l’opération précédente.

Comme vous le voyez, une fois que l’on est habitué à la syntaxe, cette nouvelle version est beaucoup moins pénible à écrire, à comprendre, et à modifier si l’on veut y rajouter des divisions successives. Le Haskell8 a même poussé les choses plus loin : lorsque la monade est définie selon certaines règles (que nous n’aborderons pas dans cette introduction), on peut écrire les choses ainsi.

divM_3 :: Double -> Double -> Double -> Double -> Maybe Double
divM_3 x y1 y2 y3 = do
    x1 <- return x
    x2 <- divM x1 y1
    x3 <- divM x2 y2
    divM x3 y3

On s’éloigne du fonctionnement réel des opérations, mais le sens de l’ensemble apparaît plus clairement. Mais ce n’est pas le seul intérêt d’une monade, et pour ainsi dire, ce serait même le plus marginal.

Une monade a surtout l’avantage de créer un contexte de programmation. Concrètement, qu’est-ce que cela signifie ? Deux choses. D’un point de vue formel, utiliser une séquence de bind permet de s’assurer que le traitement ne sorte jamais de la monade.

Si vous vous souvenez de la définition intuitive donnée au début de ce cours, une monade est une boite, dans laquelle on met des données, pour ne plus les en ressortir. En pratique, bind ressort les données de la boite, mais la fonction passée en paramètre de bind les y remet aussitôt : du point de vue du programmeur, tout se passe en coulisse.

Du point de vue du fond, réaliser un traitement au sein d’une monade donne un sens à ce traitement, sens qui dépend — précisément — de la monade employée. Par exemple, notre monade Maybe signifie « Attention, ce morceau de programme peut échouer ! ».

La monade de liste, que nous avons vue dans la première section, sert quant à elle à signaler un traitement dont chaque étape peut avoir zéro, un, ou plusieurs résultats différents, sans qu’on puisse vraiment en prévoir le nombre exact.

Par exemple, une intelligence artificielle pour jeu de Puissance 4, qui cherche à déterminer toutes les configurations possibles sur les six coups à venir, pour trouver la plus favorable : certaines branches mèneront à des culs de sac, d’autres offriront plus ou moins de possibilités de poursuivre, certaines colonnes pouvant se trouver pleines.

C’est certainement la plus importante de toutes. Une fois entré dans cette monade, il est absolument impossible d’en sortir, et elle confère le sens de « Attention ! Attention ! Comportement impur ici, on modifie le monde réel. Effets de bord à prévoir. ». C’est le moyen qu’a trouvé le Haskell de représenter avec des outils de programmation fonctionnelle pure des opérations qui génèrent des effets de bord, comme afficher un texte à l’écran ou lire le contenu d’un fichier.

Voyons cela plus en détail. Les opérations d’entrée-sortie représentent deux sévères épines dans le pied de la programmation fonctionnelle pure, et l’utilisation d’une monade permet de contourner les deux problèmes.

Tout d’abord, l’absence d’effet de bord (également appelée transparence du référentiel) a pour conséquence qu’une fonction appelée deux fois avec les mêmes paramètres donnera nécessairement deux fois le même résultat. Cela autorise donc à garder ce résultat en mémoire, plutôt que de répéter le traitement lors du deuxième appel : on nomme cela la mémoïsation.

Seulement, quid de la fonction getLine, qui lit une ligne de texte entrée par l’utilisateur ? Appelée deux fois de suite, la probabilité que l’utilisateur tape strictement la même chose la deuxième fois est infime. Mais comment savoir que cette fonction ne doit pas être mémoïsée, contrairement à toutes les autres ? En la « marquant » du sceau de l’impureté, ce qui se fait en lui donnant le type IO String plutôt que String.

Plus fourbe : la transparence du référentiel a aussi pour conséquence que si deux fonctions ne dépendent pas l’une de l’autre, l’ordre dans lequel elles sont exécutées n’a strictement aucune importance. Mais il en va tout autrement des opérations d’entrée-sortie : si vous demandez à votre utilisateur de vous fournir son nom puis son prénom, il est indispensable que vous soyez certain de l’ordre dans lequel chacun sera demandé.

En bindant les fonctions au sein de la monade IO, vous créez une dépendance artificielle entre elles, qui oblige celle que vous placez en premier dans le code à être effectivement exécutée la première. Cela peut apparaître plus clairement sur un code.

prénom = getLine
nom = getLine
main = putStrLn ("Bonjour " ++ prénom ++ " " ++ nom)
main = getLine
    `bind` \prénom -> (getLine
    `bind` \nom -> putStrLn ("Bonjour " ++ prénom ++ " " ++ nom))

Dans le premier code, prénom et nom ne dépendent pas l’une de l’autre : rien ne vous permet de savoir si votre programme va commencer par demander le prénom ou le nom9, donc si les informations fournies par l’utilisateur seront dans le bon ordre lorsque le programme les affichera à l’écran.

Dans le second, au contraire, il est indispensable que le premier getLine ait été exécuté avant que le programme puisse passer à l’exécution de l’ensemble \prénom -> […] nom)), donc au second getLine. On a réussi à forcer un ordre d’exécution, censément incompatible avec la programmation fonctionnelle pure.

Un outil multi-langages

Absolument pas ! Il est possible de créer des monades dans de nombreux langages, avec plus ou moins de difficultés. Prenons par exemple le OCaml, un autre des principaux langages fonctionnels, où les monades ne sont pourtant pas d’usage aussi courant qu’en Haskell. La monade Maybe s’implémenterait simplement comme suit10.

type 'a maybe = Nothing | Just of 'a

let return = Just

let (bind) m f =
    match m with
    | Nothing -> Nothing
    | Just a -> f a

Contrairement au Haskell, le(s) paramètre(s) des types se place(nt) avant le nom de ceux-ci, d’où la syntaxe 'a maybe. Par ailleurs, la structure match … with sert à faire du filtrage par motif, comme on l’a déjà vu. Et comme vous pouvez le constater, cela reste très facile à mettre en place.

On peut ainsi réaliser des divisions successives sur un nombre, en s’assurant qu’une division par zéro renvoie une erreur, et que celle-ci soit répercutée sur la suite des opérations. Voici un exemple de comment cela pourrait se faire.

let divM x y = if y = 0. then Nothing else Just (x /. y)

let impossible = Just 35.
                 bind fun x -> divM x 5.
                 bind fun x -> divM x 0.
                 bind fun x -> divM x 7.

Si l’on peut placer la fonction bind entre ses deux arguments, comme on le fait en Haskell, c’est parce qu’on l’a déclarée entourée de parenthèses dans le code un peu plus haut. Sans cela, la syntaxe serait très lourde.

Autre exemple, le Rust : un langage multi-paradigmes, qui intègre plutôt bien les outils de la programmation fonctionnelle. Voici comment on pourrait implémenter la monade Maybe11.

enum Maybe<A> {
    Just(A),
    Nothing
}

fn lift<A>(a : A) -> Maybe<A> {
    Maybe::Just(a)
}

impl<A> Maybe<A> {
    fn bind<B, F>(self, f : F) -> Maybe<B>
        where F : Fn(A) -> Maybe<B>    {
        match self {
            Maybe::Nothing => Maybe::Nothing,
            Maybe::Just(a) => f(a)
        }
    }
}

Quelques explications pour ne pas être bloqués par la syntaxe.

On reprendra l’exemple des divisions successives pour montrer la mise en action de cette monade.

fn divM(x : f64, y : f64) -> Maybe<f64> {
    if y == 0.0 { return Maybe::Nothing; }
    lift(x / y)
}

fn main()   {
    let m = Maybe::Just(35.0).bind(|x| divM(x, 5.0))
                .bind(|x| divM(x, 0.0))
                .bind(|x| divM(x, 7.0));

    match m {
        Maybe::Nothing => println!("Nothing"),
        Maybe::Just(ref a) => println!("Just {}", a)
    }
}

La syntaxe |x| divM(x, 5.0) permet de définir une fonction anonyme, ou closure comme elles sont appelées par les Rustacés : dans notre cas, elle prend en entrée un argument x, en l’occurrence, l’éventuel contenu de la valeur monadique, et renvoie le résultat de divM(x, 5.0), qui est bien une valeur monadique.

Comme vous pouvez le voir, le fait d’avoir fait de bind une méthode de Maybe permet de chaîner les appels à celle-ci dans une syntaxe fort élégante.

À vrai dire, il n’est même pas indispensable d’utiliser un langage fonctionnel ou intégrant fortement le paradigme fonctionnel, pour pouvoir créer une monade. Voici l’adaptation en C++11 de notre monade Maybe.

#include <functional>
#include <utility>

template <typename A>
class Maybe {

    private:
    bool _proprio{};
    A const* const _valeur{};

    public:
    constexpr Maybe() = default;
    constexpr Maybe(A const& valeur) noexcept : _valeur{&valeur}    {   }
    constexpr Maybe(A&& valeur)      noexcept
        : _proprio{true}, _valeur{new A{valeur}}
        {   }

    constexpr Maybe(Maybe const& m)  noexcept : _valeur{m._valeur}  {   }
    constexpr Maybe& operator=(Maybe const& m) noexcept {
        _proprio = false;
        _valeur = m._valeur;
        return *this;
    }

    ~Maybe() { if(_proprio) delete _valeur; }

    inline constexpr static Maybe Nothing() noexcept {
        return Maybe();
    }

    template<typename ...Args>
    inline constexpr static Maybe Just(Args&&... args) noexcept {
        return Maybe(std::forward<Args>(args)...);
    }

    template<typename F, typename ...Args>
    inline constexpr Maybe bind(F&& f, Args&&... args) const {
        if (!_valeur) return Nothing();
        return Maybe(f(*_valeur, std::forward<Args>(args)...));
    }

    A valeur() const {
        if (_valeur) return *_valeur;
        return 0;
    }
};

C’est en voyant combien le code est beaucoup plus encombré que l’on comprend que les monades ne sont pas très naturelles dans ce langage. Il me serait impossible de totalement détailler le code, alors je m’en tiendrai à signaler que template <typename A> sert à gérer les types paramétrés.

Mais une fois ce gros travail effectué en amont, et en supposant toujours notre succession de divisions, on peut alors utiliser la syntaxe suivante.

int main()  {
    auto divM  = [](auto x, auto y) {
        if (y == 0) return Maybe<double>::Nothing();
        return Maybe<double>::Just(x / y);
    };
    auto div0M = [divM](auto x){ return divM(x, 0.0); };
    auto div5M = [divM](auto x){ return divM(x, 5.0); };
    auto div7M = [divM](auto x){ return divM(x, 7.0); };

    Maybe<double>::Just(35.0).bind(div5M).bind(div0M).bind(div7M);

    return 0;
}

Si la syntaxe finale est assez élégante, il n’en reste pas moins qu’utiliser des fonctions anonymes comme dans les autres langages est une véritable plaie, et que faire une définition « propre » de notre monade Maybe est excessivement difficile. C’est pourquoi vous rencontrerez assez peu de monades dans des langages impératifs.

Mais cela arrive ! La bibliothèque ReactiveX de Java, Groovy, Clojure… offre le triplet (Observable, from, subscribe) qui constitue une monade de la plus belle espèce.

En revanche, s’il est en théorie possible de créer des monades dans un langage utilisant un système de typage dynamique, cela s’avère très artificiel, comme le montre cet exemple en Erlang.

return (A) -> {just, A}.

bind ({nothing}, _) -> {nothing};
bind ({just, A}, F) -> F(A).

divM (X, Y) when Y == 0 -> {nothing};
divM (X, Y) when Y /= 0 -> return(X / Y).

impossible() ->
    bind(
        bind(
            bind(return(35.0), fun (X) -> divM(X, 5.0) end)
        , fun (X) -> divM(X, 0.0) end)
    , fun (X) -> divM(X, 7.0) end).

Le type Maybe n’est pas défini, puisque cela ne fait pas partie de l’ordre des possibles en Erlang, et il est simulé par un tuple, soit de la forme {nothing}, soit de la forme {just, <quelque chose>}. Dès lors, on peut en effet implémenter la fonction bind à l’aide d’un simple filtrage par motif, mais il n’est fait aucune vérification sur la nature de la fonction passée en paramètre. Le résultat est une imbrication assez inélégante de bind( … ).

Cela nous amène à la constatation suivante : s’il est effectivement possible de créer des monades dans presque tous les langages, certains d’entre eux se montrent nettement rétifs à cette utilisation. Alors, est-ce un problème ?

Nécessité ?

Absolument pas. Je vais le mettre en gros, pour être sûr que le message passe bien.

Il y a plusieurs raisons à cela. Pour commencer, en Haskell, la seule monade dont le langage ne peut absolument pas se passer, c’est la monade IO. Or gérer les entrées-sorties au moyen d’une monade n’est qu’une solution parmi d’autres.

On peut bien sûr accepter d’intégrer l’impureté dans le langage, comme en OCaml, ou de faire une croix sur les interactions avec le monde extérieur, comme le langage Charity, mais ce ne sont que des pis-aller. En restant parfaitement pur, deux autres solutions se sont fait jour à l’heure actuelle.

Ensuite, même au sein du Haskell, les monades permettent de résoudre un type très spécifique de problèmes : ceux qui nécessitent de composer des fonctions de manière non triviale. Dans la pratique, vous pourrez écrire de nombreux programmes sans jamais avoir besoin de créer une monade pour arriver à vos fins.

Quant à utiliser les monades déjà présentes dans la bibliothèque standard, vous ne pourrez certainement pas échapper à la monade IO. Pour les autres, en revanche, c’est différent : les listes et le type Maybe, par exemple, sont très utiles en soi, mais les occasions où il est nécessaire d’utiliser leur aspect monadique sont en réalité assez rares.

Reste donc la monade IO. Est-il nécessaire de comprendre comment fonctionnent les monades pour s’en servir ? Non. La notation do s’abstrait presque complètement du fonctionnement réel de la monade sous-jacente, et offre une syntaxe plus claire à l’usage.


Comme je manque cruellement d’imagination pour représenter une monade d’une manière qui ne fasse pas peur, j’ai lâchement emprunté une illustration tirée de Learn You a Haskell For Great Good! de Miran Lipovača pour mon logo ; celui-ci reste donc la propriété de M. Lipovača, distribuée sous licence Creative Commons BY-SA-NC.

Merci à Vayel pour ses très nombreux commentaires en bêta, à gbdivers pour son intervention sur le code C++ et à Javier pour son exemple de ReactiveX.

Si vous n’avez pas trouvé votre bonheur avec mon explication, et que vous connaissez un peu d’anglais, vous pourrez trouver sur cette page des liens vers la quasi-totalité des cours anglophones sur les monades existant à ce jour. Environ 85 à l’heure où j’écris ces lignes. Oui.


[1]Le mot est faible…
[2]C’est la boite de la définition intuitive.
[3]On place l’objet dans la boite.
[4]La boite et son contenu.
[5]On sort le contenu de la boite, on lui applique un traitement, et on le remet dans la boite.
[6]Comme dit dans la définition intuitive, cette fonction peut modifier le type de l’objet encapsulé, ce n’est pas gênant. On peut, par exemple, avoir une fonction qui prend un flottant en entrée et renvoie un arrondi entier de celui-ci, encapsulé dans le type de la monade.
[7]Ce n’est pas fondamentalement utile, avouons-le. Mais l’exemple est simple.
[8]Et seulement lui ou les langages qui en sont dérivés, désolé.
[9]Si vous avez l’habitude de la programmation impérative, cela vous paraîtra sans doute totalement contre-intuitif, mais c’est ainsi qu’il en va en programmation fonctionnelle pure : ce n’est pas parce qu’un élément se trouve avant un autre dans l’expression qu’il sera nécessairement évalué en premier.
[10]Notez bien que la bibliothèque standard du OCaml propose le type Option qui n’est autre que Maybe sous un autre nom, mais j’ai préféré rester cohérent entre les exemples.
[11]La bibliothèque standard du Rust propose le même type Option que celle du OCaml.