Les monades sans les maths
Lecture 10 min. âą
Table des matiĂšres
- Avant propos
- Les Conteneurs : rendre immutable le monde
- Les Foncteurs : agir sur un conteneur
- Les limites des Functors
- Les Monades Ă la rescousse !
- Exemple dâutilisation des flatMap en JS
- On résume
- Lâerreur Ă un milliard de dollars
- Conclusion

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:
class Container<T> {
private readonly value : T;
constructor(value: T) {
this.value = value
}
getValue() : T {
return this.value
}
}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.
const bouteille_magique : Container<String> = new Container("đ")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
error TS2341: Property 'value' is private and only accessible within class 'Container<T>'Parfait notre alcool est sĂ©curitĂ© đŸ.
Ce qui est exellent avec cette boutteille câest quâelle peut indĂ©finiment se remplir !!
const bouteille_magique = new Container("đ")
console.log(bouteille_magique)
const verre = bouteille_magique.getValue()
const verre2 = bouteille_magique.getValue()
const verre3 = bouteille_magique.getValue()
console.log("verre 1: ", verre) // "đ"
console.log("verre 2: ", verre2) // "đ"
console.log("verre 3: ", verre3) // "đ"On peut vider autant de fois nos verres la bouteille est toujours pleine !
console.log(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:
interface Func<T,TResult>
{
(item: T): TResult;
}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:
class Functor<T> extends Container<T> {
map<R>(f: Func<T, R>) : Functor<R> {
return new Functor(f(this.getValue()))
}
}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.
const ajouter_poire = x => x + "đ"
const bouteille_magique = new Functor("đ");
console.log(bouteille_magique)
console.log(
bouteille_magique
.map(ajouter_poire)
.map(ajouter_poire)
.getValue()
); // "đđđ"
console.log(bouteille_magique.getValue()) // "đ"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 đ
const ajouter_poire = x => x + "đ"
const remplacer_par_du_chouchen = to_replace => x => x.replace(new RegExp(to_replace, "g"), "đ")
const bouteille_magique = new Functor("đ");
const poire_qui_tabasse = bouteille_magique
.map(ajouter_poire)
.map(ajouter_poire);
console.log("Poire qui tabasse avant transmutation", poire_qui_tabasse.getValue()) // Poire qui tabasse avant tansmutation đđđ
const chouchen = poire_qui_tabasse.map(remplacer_par_du_chouchen("đ"));
console.log("Chouchen", chouchen.getValue()); // Chouchen đđđ
console.log("Poire qui tabasse aprĂšs tansmutation", poire_qui_tabasse.getValue()); // Poire qui tabasse aprĂšs tansmutation đđđ
console.log("Bouteille magique", bouteille_magique.getValue()) // 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.
const contenu = new Functor("Bonjour Ă tous");
const html = contenu
.map(content => `<h1>${content}</h1>`)
.map(content => `<body>${content}</body>`)
.map(contenu => `<html>${contenu}</html>`);
console.log(html.getValue()) // <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.
const ajouter_poire = x => new Functor(x + "đ")
const log = step => x => {
console.log(`contenu de la bouteille étape ${step}: ${x}`, )
return x
}
const bouteille_magique = new Functor("đ");
const poire_qui_tabasse = bouteille_magique
.map(log(1)) // contenu de la bouteille Ă©tape 1: đ
.map(ajouter_poire)
.map(log(2)) // contenu de la bouteille Ă©tape 2: Functor ( đđ )
.map(ajouter_poire)
.map(log(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.
export default class Monad<T> extends Container<T> {
map<R>(f: Func<T, R>) : Monad<R> {
return new Monad(f(this.getValue()))
}
flatMap<R> (f: Func<T, Monad<R>>) : Monad<R> {
return f(this.getValue())
}
}Avec notre nouvelle Monad fraßchement définie on peut régler notre problÚme de poires dans des bouteilles en 4 dimensions.
const ajouter_poire = x => new Monad(x + "đ")
const log = step => x => {
console.log(`contenu de la bouteille étape ${step}: ${x}`, )
return x
}
const bouteille_magique = new Monad("đ");
const poire_qui_tabasse = bouteille_magique
.map(log(1)) // contenu de la bouteille Ă©tape 1: đ
.flatMap(ajouter_poire)
.map(log(2)) // contenu de la bouteille Ă©tape 2: đđ
.flatMap(ajouter_poire)
.map(log(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.
const ajouter1 = x => x + 1Si on lâapplique:
const result = [0]
.map(ajouter1)
.map(ajouter1)
console.log(result) // [2]On obtient bien le résultat attendu qui est un tableau de 1 élement valant 2.
Mais si par contre on fait
const ajouter1Wrapped = x => [x + 1]const result = [0]
.map(ajouter1Wrapped)
.map(ajouter1Wrapped)
console.log(result) // [ [ '11' ] ]Bon lĂ par contre câest plus exactement ce quâon veut đ€
Mais si on remplace par flatMap magie ! đ„ł
const result = [0]
.flatMap(ajouter1Wrapped)
.flatMap(ajouter1Wrapped)
console.log(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:
interface OptionSome<T> {
value: T
}
interface OptionNone<T> {}
type IOption<T> = OptionSome<T> | OptionNone<T>
class Option<T> {
private option : IOption<T>;
static Some<U>(value: U) : Option<U> {
return new Option({value})
}
static None<U>() : Option<U> {
return new Option({})
}
public isOk() : Boolean {
return (this.option as OptionSome<T>).value !== undefined
}
private constructor (option : IOption<T>) {
this.option = option
}
public orElse(fallback: T) : T {
if (this.isOk()) {
return (this.option as OptionSome<T>).value;
}
return fallback
}
public toString() : String {
if (this.isOk()) {
return `Some(${(this.option as OptionSome<T>).value})`
}
return "None"
}
}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:
function getFirstEven(tab: Array<number>) : number {
for (let i of tab) {
if (i % 2 == 0) {
return i
}
}
}
const result1 = getFirstEven([1, 3, 5, 8])
console.log(result1 + 1) // 9
const result2 = getFirstEven([1, 3, 5, 9])
console.log(result2 + 1) // NanDans 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.
let result2 = getFirstEven([1, 3, 5, 9]);
if(!isNaN(result2)) {
result2 = 0
}
console.log(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.
function getFirstEven(tab: Array<number>) : Option<number> {
for (let i of tab) {
if (i % 2 == 0) {
return Option.Some(i)
}
}
return Option.None()
}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.
const with_even = getFirstEven([1, 3, 5, 8]);
const without_even = getFirstEven([1, 3, 5, 9]);
console.log(`${with_even.orElse(0) + 1}`); // 9
console.log(`${without_even.orElse(0) + 1}`); // 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:
class Option<T> {
// début de la classe ....
map<R>(f: Func<T, R>) : Option<R> {
if (!this.isOk()) return Option.None()
return Option.Some(f((this.option as OptionSome<T>).value))
}
flatMap<R> (f: Func<T, Option<R>>) : Option<R> {
if (!this.isOk()) return Option.None()
return f((this.option as OptionSome<T>).value)
}
// reste de la classe ....
}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.
const log = step => x => {
console.log(`step ${step}: ${x}`)
return x
}Un exemple dâechec
let a = Option.Some(1)
.map(log(0))
.flatMap(x => (x % 2 == 0) ? Option.Some(x) : Option.None<number>())
.map(log(1))
.map(x => x + 2)
.map(log(2))
.orElse(666);
console.log(a) // 666La console nous affiche
step 0: 1
666On remarque que les appels ont été court-circuités pour atteindre le orElse qui lui défini sa valeur de 666.
let b = Option.Some(0)
.map(log(0))
.flatMap(x => (x % 2 == 0) ? Option.Some(x) : Option.None<number>())
.map(log(1))
.map(x => x + 2)
.map(log(2))
.orElse(666);
console.log(b); // 2La console nous affiche
step 0: 0
step 1: 0
step 2: 2
2Ici 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.
interface ResultError<E> {
error: E
}
interface ResultOk<T> {
value: T
}
type IResult<T,E> = ResultOk<T> | ResultError<E>
class Result<T, E> {
private result : IResult<T, E>;
static Ok<U, E2>(value: U) : Result<U, E2> {
return new Result({value})
}
static Err<U, E2>(error: E2) : Result<U, E2> {
return new Result({error})
}
isErr() : Boolean {
return (this.result as ResultError<E>).error !== undefined
}
private constructor(result: IResult<T, E>) {
this.result = result
}
toString() : String {
if (this.isErr()) {
return `Error ( ${(this.result as ResultError<E>).error} )`
}
return `Ok ( ${(this.result as ResultOk<T>).value} )`
}
}On peut lâutiliser pour par exemple gĂ©rer la connexion Ă une base de donnĂ©es.
function getDataBaseConnection(url : String) : Result<Connection, String> {
try {
const connection : Connection = open(url);
return Result.Ok(connection);
} catch(e) {
return Result.Err(e.message);
}
}Cela permet de facilement gérer les erreurs et surtout de ne pas oublier de les gérer !
const resultConnection = getDataBaseConnection("database");
console.log(resultConnection); // Ok( Connection { url : "database" } )
const resultConnection = getDataBaseConnection("wrong");
console.log(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.
class Result<T, E> {
// début de la classe ....
map<R>(f: Func<T, R>) : Result<R, E> {
if (this.isErr()) return Result.Err((this.result as ResultError<E>).error)
return Result.Ok(f((this.result as ResultOk<T>).value))
}
flatMap<R> (f: Func<T, Result<R, E>>) : Result<R, E> {
if (this.isErr()) return Result.Err((this.result as ResultError<E>).error)
return f((this.result as ResultOk<T>).value)
}
// reste de la classe ....
}Et utilisé ainsi
const ok = Result.Ok<number, String>(31)
.map(x => x + 1)
.flatMap(x => Result.Ok(x + 10));
console.log(ok); // Ok( 42 )
const ok = Result.Err<number, String>("bad value")
.map(x => x + 1)
.flatMap(x => Result.Ok(x + 10));
console.log(ok); // Error( bad value )On peut aussi sur cette, maintenant véritable monade, rajouter une méthode match.
class Result<T, E> {
// début de la classe ....
match<R>(resolve: Func<T, R>, reject: Func<E, R>) : R {
if (this.isErr()) {
return reject((this.result as ResultError<E>).error)
}
return resolve((this.result as ResultOk<T>).value)
}
// reste de la classe ....
}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.
const code : number = getDataBaseConnection("database")
.match(
(connection: Connection) => {
// c'est good đ
return 1;
},
(error: String) => {
// traitement de l'erreur đ
return -1;
}
)Il est aussi possible de transformer un Result en une Option.
class Result<T, E> {
// début de la classe ....
ok() : Option<T> {
if(this.isErr()) {
return Option.None()
}
return Option.Some((this.result as ResultOk<T>).value)
}
// reste de la classe ....
}Toute erreur fini en None. Et tout succĂšs en Some.
const connection : Option<Connection> = getDataBaseConnection("database").ok();
console.log(connection); // Some ( Connection { url: "database" } )
const connectionFail : Option<Connection> = getDataBaseConnection("database").ok();
console.log(connectionFail); // NoneCela 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 đ
