Les monades sans les maths
Pour tout dire, une monade dans X est juste un monoïde dans la catégorie des endofunctors de X, avec des produits de X remplacé par la composition d'endofunctors et de l'unité de l'identité endofunctor.
Si vous avez déjà regardé de la programmation fonctionnelle au moins une fois, vous êtes surement tombé sur cette blague de nerd barbu.
Et vous vous êtes surement dit le fonctionnel c'est pas pour moi c'est trop compliqué. Alors on va essayer de démystifier tout ça.
Avant propos
La suite de l'article se fera en Typescript.
Pourquoi lui et pas un autre? Déjà c'est celui que je maîtrise le mieux, ensuite c'est celui qui permet le pont le plus rapide entre le non fonctionnel et le fonctionnel tout en étant suffisamment typé pour être représentatif.
Ceci n'est pas non plus un cours de Typescript, je ne reviendrait donc pas sur les concepts et la grammaire du langage, je suppose que vous connaissez le langage.
On m'a aussi fait remarquer que mon article était inspiré voir très inspiré de l'Apéro Fonctionnel.
J'avais complètement oublié son existence par contre apparemmment les exemples m'avait marqué, comme vous allez pouvoir le voir par la suite 🤣.
Donc rendons à César ce qui est a César, merci à Quentin Adam, Philippe Charrière, Etienne Issartial, Thomas Guenoux, Nicolas Leroux; de m'avoir à l'époque fait découvir tous ces concepts que j'ai muri pendant plus de 3 ans 🥳.
Aussi cet article doit être pris en compte comme un état de l'art de mes connaissances du fonctionnel à l'heure où j'écris cet article.
Ces précautions prises, je vous souhaite une bonne lecture 😉
Les Conteneurs : rendre immutable le monde
Le premier concept que l'on va découvrir est ce que l'on appelle les containers. Rien à voir avec les conteneurs de Docker.
On est entre gens sérieux 😋 Non moi je vous parle des conteneurs fonctionnels.
La définition d'un conteneur est quelque chose de plutôt simple, il s'agit d'une boîte où on ne peut pas modifier ce qu'il y a dedans.
Exemple:
Voici une implémentation possible d'un conteneur, une propriété privée value
est initialisée au travers d'un constructeur et possède un acesseur.
Pour utiliser ce conteneur, vous pouvez écrire quelque chose comme:
Je sais que vous aimez l'alcool, pardon pour ceux qui ne l'apprécie pas ^^". Imaginez unez bouteille de poire qui aurait la capacité fort appréciable de ne jamais être vide.
Deux choses sont à remarquer ici. Tout d'abord la valeur "chat" ne pourra plus jamais être modifié. En effet value
est privée et ne possède pas de mutateur.
Si vous tentez des fantaisies comme remplacer la poire par de la vodka:
bouteille_magique.value = "🥔"
Vous allez vous manger des erreurs de compilation
Parfait notre alcool est sécurité 🍾.
Ce qui est exellent avec cette boutteille c'est qu'elle peut indéfiniment se remplir !!
bouteille_magique
"verre 1: ", verre // "🍐"
"verre 2: ", verre2 // "🍐"
"verre 3: ", verre3 // "🍐"
On peut vider autant de fois nos verres la bouteille est toujours pleine !
bouteille_magique // Container { value : "🍐" }
Les Foncteurs : agir sur un conteneur
Bon c'est génial on a quelque chose dans une boîte mais on ne peut plus rien en faire. Alors c'est cool les boîtes mais dans la vraie vie c'est un peu limité ...
C'est là que vienne à la rescousse les Functors
, qui sont des Containers
munie d'une fonction map
D'abord un petit sucre syntaxique:
Cela définie une fonction qui prend un type T
en paramètre et renvoie un type R
.
Le Functor
en lui-même est extrêment concis, voyez plutôt:
La fonction map
prend en paramètre une fonction f
et retourne un nouveau Functor
d'un nouveau type <R>
.
Il s'utilise comme un Container
normal à ceci près qu'il est capable de modifier les données de Container
, ou plutôt de renvoyer un clone modifié.
Reprenons notre bouteille magique. Imaginons que notre poire n'est pas suffisamment "poirée" à notre goût.
;
bouteille_magique
bouteille_magique
ajouter_poire
ajouter_poire
; // "🍐🍐🍐"
// "🍐"
En effet la valeur contenu dans bouteille_magique
ne change pas. On conserve l'immutabilité tout en permettant une évolution contrôlée.
Et maintenant en tant que breton je veux remplacer la 🍐 par de la 🍎
;
ajouter_poire
ajouter_poire;
"Poire qui tabasse avant transmutation", // Poire qui tabasse avant tansmutation 🍐🍐🍐
;
"Chouchen", ; // Chouchen 🍎🍎🍎
"Poire qui tabasse après tansmutation", ; // Poire qui tabasse après tansmutation 🍐🍐🍐
"Bouteille magique", // Bouteille magique 🍐
La bouteille magique demeure inchangée de même que la bouteille de poire "améliorée" 🥴🥳.
Bon pour ceux qui n'aime pas trop l'alcool j'ai un exemple plus technique.
Vous avez surement rencontré le problème d'entourer un texte de balises HTML.
Les Functors
peuvent de manière très pratique vous aider à résoudre ce problème.
;
`<h1> </h1>`
`<body> </body>`
`<html> </html>`;
// <html><body><h1>Bonjour à tous</h1></body></html>
Les limites des Functors
Comme on vit dans un monde peuplé d'imperfections les Functors
ne font pas exception.
Il existe un cas qui va nous poser des problèmes imaginez que dans méthode map vous décidiez de renvoyer un Functor
.
;
1 // contenu de la bouteille étape 1: 🍐
ajouter_poire
2 // contenu de la bouteille étape 2: Functor ( 🍐🍐 )
ajouter_poire
3; // contenu de la bouteille étape 3: Functor ( Functor ( 🍐🍐 ) 🍐 )
Tout se passe comme si vous aviez des bouteilles de Klein d'eau de vie. Vous êtes capable de mettre des bouteilles dans des bouteilles grâce à la 4ème dimension !!
Bon vu que c'est pas du tout le résultat qu'on attendait il va falloir trouver un moyen de régler ce problème.
Les Monades à la rescousse !
L'idée est d'éviter d'entourer un Functor
d'un autre Functor
à chaque appel de map
, pour cela on va définir une méthode flatMap
qui a pour but d'extraire le contenu du Functor
avant d'appliquer la méthode f
de transformation.
Pour cela on va définir un Functor
disposant d'une méthode flatMap
qui réalisera les actions citées ci-dessus.
Avec notre nouvelle Monad
fraîchement définie on peut régler notre problème de poires dans des bouteilles en 4 dimensions.
;
1 // contenu de la bouteille étape 1: 🍐
ajouter_poire
2 // contenu de la bouteille étape 2: 🍐🍐
ajouter_poire
3; // contenu de la bouteille étape 3: 🍐🍐🍐
Et voilà retour dans la 3ème dimension 😎
Exemple d'utilisation des flatMap en JS
On va prendre pour exemple la méthode ajouter1
.
Si on l'applique:
ajouter1
ajouter1
result // [2]
On obtient bien le résultat attendu qui est un tableau de 1 élement valant 2
.
Mais si par contre on fait
ajouter1Wrapped
ajouter1Wrapped
result // [ [ '11' ] ]
Bon là par contre c'est plus exactement ce qu'on veut 🤔
Mais si on remplace par flatMap
magie ! 🥳
ajouter1Wrapped
ajouter1Wrapped
result // [ 2 ]
La flatMap
extrait le contenu du tableau avant d'appliquer l'opération de la méthode du map
puis réinsère le résultat dans le tableau.
On résume
Un Container
est une boîte qui peut contenir n'importe quoi mais dont le contenu ne peut plus être changé une fois à l'intérieur.
Un Functor
est un Container
muni d'une méthode map
qui permet de renvoyer un clone modifié du Container
originel.
Une Monade
est un Functor
muni d'une méthode flatMap
qui avant d'appliquer une transformation sur le contenu d'un Container
l'extrait avant.
L'erreur à un milliard de dollars
Si vous faîtes du java ou du javascript vous avez surement déjà rencontré des erreurs du type NullPointerException
, VM166:1 Uncaught ReferenceError: x is not defined
.
Toutes ces erreurs peuvent se rassembler sous une même typologie appellée les accès à des références non définies ou non autorisées. Ce qui provoque dans la plupart des cas l'arrêt du programme au runtime. Ce qui est on va pas se le cacher un peu gênant, surtout pour l'utilisateur 😋
Pour éviter ce genre de désagrément la programmation fonctionnelle, fourni aux développeurs des outils qui vont le forcer à gérer manuellement tout les cas dont les cas d'erreurs. Ces outils sont bien évidemment des Monades
.
L'Option quand on sait pas si la valeur de retour existe
Parfois une fonction peut ne pas renvoyer le résultat escompté. On appelle ça plus communément une erreur.
Il existe de nombreuse façon de traiter une erreur.
La plus répandue est de gérer des exceptions qui vont remonter dans la pile d'appel jusqu'à être capturée.
Cela à un inconvénient majeur qui est de ne pas forcer à traiter l'erreur. En effet si l'erreur n'est pas gérée, elle va remonter j'usqu'à faire crasher l'application.
Une autre manière est de définir une pair de sortie, c'est qui a été utilisé par Go, de la même manière on a aucune obligation de gérer l'erreur. Elle peut donc avoir des effets de bords non désirés.
L'Option
est un mécanisme qui force le développeur à traiter au plus vite une erreur sans risque d'oubli malencontreux.
Il est possible d'implémenter cette Monade
en Typescript, comme ci-dessous:
Imaginons que vous avez une liste d'entiers, vous souhaitez récupérer le premier nombre pair de la liste.
Typiquement si un développeur n'est pas consencieux on peut se retrouver avec ce genre de problème:
result1 + 1 // 9
result2 + 1 // Nan
Dans certains cas le résultat n'est pas défini ce qui s'il est utilisé dans la suite du programme sans vérification préalable cela peut occasionner des soucis voir des crashs de l'application.
Pour éviter un crash on est obligé de faire des vérifications supplémentaires.
;
if!result2
result2 + 1
Le fonctionnel à la rescousse
On peut réécrire cette fonction en retournant à la place d'un number
, un Option<number>
qui indique explicitement au développeur qu'il doit gérer le cas d'erreur.
Grâce à la monade Option
, il est possible de traiter le cas où il n'y a aucun nombre pair dans la liste passer en paramètre et de définir une valeur de fallback dans ce cas.
;
;
``; // 9
``; // 1
Ce qui permet de se passer de if (value === null)
ou pire d'oublier complètement de traiter le cas d'erreur.
En faire un Monade qui se respecte
Si vous avez bien suivi les précédent paragraphe, vous remarquez l'absence des méthodes map
et flatMap
, ce n'est donc pas une Monade
😱
On peut remédier à ce problème de la manière suivante:
On remarque que la variante None
est traitée différemment, en effet si on rencontre une variante None
on renvoie automatiquement une variante None
, ceci permet de court-circuiter un traitremant en l'absence de résultat.
On définit une méthode log
permettant de se rendre compte de qui se passe dans la pile d'appel.
Un exemple d'echec
0
x % 2 == 0 ? x :
1
x + 2
2
666;
a // 666
La console nous affiche
step 0: 1
666
On remarque que les appels ont été court-circuités pour atteindre le orElse
qui lui défini sa valeur de 666
.
0
x % 2 == 0 ? x :
1
x + 2
2
666;
b; // 2
La console nous affiche
step 0: 0
step 1: 0
step 2: 2
2
Ici tout se passe bien la valeur de fallback n'est pas retournée, on arrive bien jusqu'à la map(log(2))
La Monade Result : gérer les erreurs
Une autre Monade
très utile est la monade Result
, comme vous allez pouvoir le remarquer, elle est très proche de la monade Option
.
On peut l'utiliser pour par exemple gérer la connexion à une base de données.
Cela permet de facilement gérer les erreurs et surtout de ne pas oublier de les gérer !
;
resultConnection; // Ok( Connection { url : "database" } )
;
resultConnection; // Error ( Unable to connect to wrong )
Notre objet resultConnection
porte en lui à la fois l'état du retour de la méthode mais aussi en cas de succès l'objet Connection
en lui même ou en cas d'erreur, le message d'erreur.
Comme pour tout à l'heure il nous manque les méthodes map
et flatMap
.
Allons-y, implémentons donc tout ça.
Et utilisé ainsi
x + 1
x + 10;
ok; // Ok( 42 )
x + 1
x + 10;
ok; // Error( bad value )
On peut aussi sur cette, maintenant véritable monade, rajouter une méthode match
.
Celle-ci prend deux fonctions en paramètre resolve
et reject
.
- La première est appelée avec la valeur du
Result
- La seconde avec l'erreur du
Result
La contrainte est de renvoyer le même type de retour dans les deux branches.
,
Il est aussi possible de transformer un Result
en une Option
.
Toute erreur fini en None
. Et tout succès en Some
.
;
connection; // Some ( Connection { url: "database" } )
;
connectionFail; // None
Cela peut-être pratique pour ne pas s'encombrer d'une erreur mais conserver tout de même le fait que quelque chose ne s'est pas déroulé de la bonne manière.
Conclusion
Je remercie tous ceux qui sont arrivés jusqu'au bout de ce long article.
Il risque d'y en avoir d'autres, la programmation fonctionnelle me passionne de plus en plus. 😍
A plus pour d'autres articles sur le fonctionnel ou autre chose 😉
Ce travail est sous licence CC BY-NC-SA 4.0.