JavaScript : Programmation fonctionnelle avec les monades
Introduction : la programmation fonctionnelle en JavaScript
Cet article s’adresse aux développeurs junior ou expérimentés désireux d’aller plus loin dans le paradigme de programmation fonctionnelle et plus particulièrement avec les “Monades”. Pour ceux qui ne connaissent pas ce paradigme, je vous recommande vivement mon précédent article sur le sujet : “La programmation fonctionnelle en JavaScript” qui se contente de poser les bases du paradigme.
Je vais également tenter de faire le parallèle entre les mathématiques (loi de composition et théorie des catégories) et une manière de l'implémenter en JavaScript. Je dis “une manière” car il en existe plein de différente pour arriver à un même comportement.
Avant d'entrer dans le vif du sujet, voici d'abord un bref rappel sur les notions essentielles du paradigme de programmation fonctionnelle qui doivent à tout prix être acquises avant d'aller plus loin.
Rappel : programmation fonctionnelle, la composition
Voici juste un petit rappel de ce qu'est la composition en programmation fonctionnelle et à quoi ça sert.
Prenons une donnée - noté ici “data” - dont le besoin est de la faire transiter au travers de plusieurs fonctions - noté ici “f”, “g” et “h”.
const result = h(g(f(data)));
// Avec Ramda, on pourrait écrire cela de manière plus élégante :
const pipeline = R.compose(h, g, f); // ou R.pipe(f, g, h);
const result = pipeline(data);
Imaginez une donnée de type “String” pour laquelle il est nécessaire d’appliquer plusieurs fonctions de transformation avant d'arriver au résultat final :
const cars = "Audi 252ch,180 Bmw,Opel 140,125 Citroën";
Disons que l'objectif est de formater la chaîne de caractère pour obtenir quelque chose de plus standard et/ou plus facile à lire :
const expectedCars = "Audi (252 ch), Bmw (180 ch), Opel (140 ch), Citroën (125 ch)";
Nous pourrions ainsi créer une liste de fonctions qui auraient chacune 1 seule responsabilité, par exemple : “split”, “parse” et “format”.
- Responsabilité de la fonction “split” : séparer la chaîne “cars” en plusieurs occurrences de type “string” par rapport au caractère de séparation “,”.
- Responsabilité de la fonction “parse” : transforme chaque occurrences de type “string” en objet de la forme { brand, power }.
- Responsabilité de la fonction “format” : transforme le tableau d'objet en chaîne de caractères.
const pipeline = compose(format, parse, split);
const finalStrResult = pipeline(cars);
Tout l'intérêt de la programmation fonctionnelle réside dans le fait que les fonctions sont pures, donc sans effets de bord. Elles sont faciles à tester et surtout, elles sont génériques pour pouvoir être ré-utilisées à des fins de composition ultérieures. De plus, pour faire de la composition de fonctions, il faut que vos fonctions soient des fonction unaires (ou unary functions en anglais), c'est-à-dire, avec un seul paramètre en entrée - on dit de ce type de fonction qu'elles ont une arité de 1. Si ce n'est pas le cas, servez-vous de la currification (ou currying en anglais) qui vous permettra d'obtenir une nouvelle fonction tant que tous les paramètres n'auront pas été passés à la fonction de base.
Malheureusement, toutes les fonctions ou syntaxes ne sont pas composables. Il faut donc créer une fonction d'ordre supérieur avec une aritée de 1 pour y mettre la syntaxe non composable afin de pouvoir faire de la composition avec cette dernière.
Rappel : programmation fonctionnelle, le “Point Free”
Une autre notion importante de la programmation fonctionnelle est le “point free”. C'est grâce à cette notion que vous serez en mesure de baisser de manière drastique la complexité de vos programmes. Vous vous apercevrez que cette notion de “Point Free” peut-être poussée à l'extrême jusqu'à vous retrouver avec un programme qui n'utilise jamais de variable (ou presque) au profit de l'enchaînement de fonctions grâce, notamment, aux fonctions de composition telles que “compose” ou “pipe” disponible avec les librairies de programmation fonctionnelle (Lodash FP ou Ramda)
Pour ceux qui auraient besoin d'un petit rappel sur ce qu'est exactement le "Point Free", je vous renvoi à mon précédent article : “La programmation fonctionnelle en JavaScript”
Qu’est-ce qu’une monade ?
Voici ce à quoi ressemble une monade.
const Id = value => ({
// Renvoi le résultat de la fonction dans son conteneur
map: f => Id.of(f(value)),
// Renvoi le résultat de la fonction - Extraction de la valeur
chain: f => f(value),
// Uniquement pour des besoin de debug
toString: () => `Id(${ value })`
});
Id.of = Id;
Ne prêtez pas attention (pour l’instant) à la manière dont est implémentée la monade Identity. Cela nous servira de base de code pour comprendre les 3 lois qui régissent la monade. Nous verrons ensuite comment l’utiliser et dans quel but.
Et en pratique, c’est quoi une monade ?
Voyez la monade comme un “pattern” ou un objet. Un objet qui contient un ensemble de fonctions qui ont chacune un comportement spécifique et identique quel que soit le type de Monade utilisée.
C’est un nouveau type de donnée. Au même titre que les types “array”, “string” ou encore “number” possèdent des méthodes spécifiques, la monade possède elle aussi des méthodes qui lui sont propres.Voyez cela un peu comme une interface ou encore comme une boite à outil du développeur dont les méthodes de bases adoptent toujours les mêmes comportements. Toutefois, si la monade garantie un certain comportement, il n’y a aucune spécification visant à uniformiser le nom de ces méthodes.
Mais alors, de quoi est constituée une monade ?
Les fonctions de base d’une monade
Pour qu’une monade existe, elle se doit d’avoir les fonctions de base qui respectent les 3 lois dites “les lois monadiques” (ou monads laws). La monade la plus simple, c’est-à-dire celle qui n’implémente que les fonctions de base, est la monade Identity (ou Identity Functor).
Voici les fonctions qu’elle contient et que vous retrouverez dans n’importe quel type de Monade :
1 - Une fonction permettant d’appliquer une fonction à une valeur et de retourner le résultat de cette fonction, c’est la fonction “fold” (ou encore extract ou bind)
Egalement appelé loi d’identité à gauche ou Left Identity law. Notation mathématique :
unit(x). chain(f) ==== f(x)
2 - Une fonction permettant d’appliquer une fonction à une valeur et de retourner le résultat, lui-même, encapsulé dans son conteneur, en l'occurrence la monade elle-même - cela permet de chaîner les traitements et ainsi appliquer plusieurs traitements à la suite de manière linéaire. C’est la fonction “map”.
Egalement appelé loi d’identité à droite ou Right Identity law. Notation mathématique :
m. chain(unit) ==== m
Voilà pour les fonctions de base. Néanmoins, je vous ai parlé de 3 lois. Nous avons vu “Left Identity” et “Right Identity”.
3 - La dernière étant la loi d’associativité ou Associativity law où la notation mathématique est la suivante :
m. chain(f). chain(g) ==== m. chain(x => f(x). chain(g)
Prenons un contexte différent pour comprendre la loi d’associativité. Prenons une opération simple telle que l’addition :
a + (b + c) = (a + b) + c
En effet, on peut vérifier très facilement la loi d'associativité avec ce genre d'opération très simple :
5 + (2 + 3) = 5 + 5 = 10
(5 + 2) + 3 = 7 + 3 = 10
Regardez la position des parenthèses - peu importe la priorité de calcul, le résultat final reste toujours le même.
Ou la multiplication :
a x (b x c) = (a x b) x c
On peut également le vérifier très facilement :
5 x (2 x 3) = 5 x 6 = 30
(5 x 2) x 3 = 10 x 3 = 30
En conclusion, l’addition et la multiplication sont des opérations qui respectent la loi d’associativité.
Et bien cette loi d'associativité utilisée dans le contexte d’une monade apporte le même comportement mais appliqué aux fonctions.
(f ∘ g) ∘ h = f ∘ (g ∘ h)
Le caractère “∘” désigne la composition. f ∘ g signifie que l’on réalise une composition de la fonction “f” avec la fonction “g”.
Prenons les fonctions “add5”, “add2” et “add3” :
const add5 = (x) => x + 5;
const add2 = (x) => x + 2;
const add3 = (x) => x + 3;
Composer les fonctions "add5", "add2" et "add3" revient à exécuter add5(x) puis add2(y) où “y” est le résultat de add5(x), puis add3(z) où z est le résultat de add2(y).
Autrement dit, cela revient à faire add3(add2(add5(x))) et la loi d’associativité nous dit qu’on doit pouvoir inverser ou changer l’ordre des fonctions tout en continuant d’obtenir le même résultat. C’est-à-dire que add3(add2(add5(x))) doit être égal à add5(add3(add2(x))).
Avec la fonction “compose” de Ramda, on peut l’écrire de la manière suivante :
compose(add5, add2, add3) === compose(add3, add5, add3)
Enfin, pour des questions pratiques, on ajoute également une fonction de debug permettant de loguer la valeur contenu dans la monade. Cela permet de connaître le type de monade utilisée ainsi que la valeur contenue dans celle-ci. C’est la fonction “toString” (ou encore log, ou inspect)
Exemple d’une monade en JavaScript
Nous donnerons à notre monade Identity le nom de “Box” pour insister sur le fait que la monade est une espèce de boîte ou conteneur - mais vous verrez plus généralement le nom "Id" pour "Identity". Sachez qu’il y a plein d’implémentations différentes mais je choisi celle-ci qui est concise et facile à appréhender :
const Box = (x) => ({
fold: (f) => f(x),
map: (f) => Box(f(x)),
toString: `Box(${x})`
});
La boîte ou “Box” fait office de conteneur pour la donnée se trouvant à l’intérieur. Le seul moyen d’influer sur cette donnée est d’utiliser un “functor’, ou plus simplement une des fonctions disponible au sein de la monade, c’est-à-dire, soit la fonction “map” en passant une fonction et qui aura pour but de transformer cette donnée tout en renvoyant une nouvelle monade - soit la fonction “fold” qui renverra le résultat de la fonction passé en paramètre.
Autrement dit, la fonction “fold” est utilisée en dernier lieu pour extraire la valeur de son conteneur (en l’occurrence, la monade) à des fins d'affichage. Attention, après cela, il n’est plus possible de chaîner les traitements. En effet, il n'y a plus de monade puisque nous avons choisi d'utiliser cette fonction "fold" pour extraire la valeur de son conteneur.
Nous pouvons ainsi écrire un programme de la sorte pour chaîner plusieurs opérations à la suite :
Box(2)
.map(add2)
.map(add5)
.map(add3);
Cette exécution renvoi une monade de type "Box" contenant la valeur 10. Le résultat ressemblera à quelque chose de ce genre => Box(10)
Lorsque le calcul est terminé, nous n’avons plus besoin de chaîner les opérations - il est alors possible de remplacer la dernière fonction “map” par la fonction “fold” pour extraire la donnée de sa boîte. Nous aurons alors :
Box(2)
.map(add2)
.map(add5)
.fold(add3);
Cette exécution renverra directement la valeur 10.
Bon d’accord, pour des opérations très simples comme la multiplication et l’addition, la syntaxe est clairement exagérée mais cela a le mérite de mettre en évidence une certaine symétrie. En effet, dans toute symétrie on retrouve une valeur ainsi que son opposée. Ici c’est à peu de chose près pareil : une fonction pour mettre une valeur dans la boite ainsi que son opposé : une fonction pour extraire la valeur de sa boite.
Voyons un cas un peu plus complexe mélangeant différentes syntaxes pour vous montrer .
A quoi sert une monade et dans quel contexte l'utiliser ?
La monade permet de créer du code linéaire (sans dépendance et sans indentation) et elle a la particularité de pouvoir chaîner des traitements et/ou syntaxes qui, initialement, ne sont pas composables.
D'autre part, en programmation fonctionnelle, il faut à tout prix éviter les fonctions qui provoquent des effets de bords sauf qu’il est impossible de faire un programme constitué uniquement de fonctions pures ! En effet, un programme sans effet de bord serait un programme qui fait des calculs dans son coin sans afficher quoi que ce soit à l’utilisateur, sans enregistrer quoi que ce soit en base de données ni écrire quoi que ce soit dans un fichier. Bref, un programme uniquement constitué de fonctions pures ne servirait à rien en l’état. En revanche, il convient de mettre de côté les fonctions ayant des effets de bords afin de maîtriser au mieux les impacts de ces effets de bords sur le reste du programme.
Et bien la monade est une solution à cette problématique. D'où la notion de conteneur. Tant qu'un traitement est nécessaire sur la valeur, on la laisse dans la boîte pour être sûr que rien ni personne ne pourra venir altérer cette valeur. Vous l'avez compris, on applique un modification sur la valeur en passant une fonction par l'intermédiaire du "functor" - la fonction "map". C'est seulement une fois que le traitement est terminé qu'on peut se permettre d'extraire la valeur pour l'afficher, la mettre en base de donnée ou dans un fichier.
Pour la monade “Maybe” par exemple, il s’agit d’une manière élégante de gérer les cas d’erreurs engendrés par des variables “nullable”, c’est-à-dire “undefined” ou “null”.
Pour la monade “Either”, cela permet l’implémentation de multiples branches (conditions) mais de manière plus élégante qu’une suite et/ou imbrication de plusieurs blocs “if”.
Dans l’exemple ci-dessous, nous nous contenterons d’utiliser la monade la plus simple pour rendre la fonction “nextCharForNumberString” plus lisible :
const nextCharForNumberString = (str) => {
const trimmed = str.trim(); // syntaxe de la méthode
const number = parseInt(trimmed, 10); // utilisation d’une fonction
const nextNumber = new Number(number + 1); // syntaxe constructeur
return String.fromCharCode(nextNumber); // fonction prototype
};
nextCharForNumberString(“ 64 “); // nous donne “A”
Remarque : vous noterez que les 3 constantes déclarées au sein de la fonction auront la même durée de vie que la fonction parente. En effet, le garbage collector ne pourra libérer la mémoire de ces constantes que lorsque la fonction parente sera terminée, en l'occurrence, lorsque la fonction “nextCharForNumberString” sera terminée.
Exemple d’utilisation d’une monade en JavaScript
Reprenons notre fonction “nextCharForNumberString” mais cette fois, en utilisant la monade “Box” créée précédemment :
const nextCharForNumberString = (str) =>
Box(str)
.map(x => x.trim())
.map(trimmed => parseInt(trimmed, 10))
.map(number => new Number(number + 1))
.fold(String.fromCharCode);
Remarque : dans chaque fonction passée en paramètre de la fonction “map”, vous voyez qu’on travaille avec le strict nécessaire. Lorsque la fonction se trouvant à l'intérieure de la 1ère fonction “map” à été exécutée, le garbage collector peut libérer la mémoire de cette constante. On optimise ainsi l’utilisation de la mémoire et la lecture est bien plus facile car chaque fonction n’a qu’une seule responsabilité.
D'autre part, vous voyez que les fonctions utilisées dans les fonctions “map” n’ont aucune dépendances entre elles, ce qui permet entre autres, de les tester unitairement très facilement ou de les réutiliser ultérieurement pour d'autres besoins.
Astuce : notez que la fonction “compose” pourrait être implémentée facilement grâce à une Monade.
On pourrait se créer notre propre fonction “compose” avec la monade “Box” :
const compose = (g, f) => (val) =>
Box(val)
.map(f)
.fold(g);
Souvenez-vous, sans l’aide de la monade (cf. article sur la programmation fonctionnelle en JavaScript), la fonction “compose” pouvait s’écrire de cette manière :
const compose = (fn2, fn1) => (val) => fn2(fn1(val));
Je ne sais pas vous, mais perso, je préfère la syntaxe avec la Monade.
Bien entendu, l’objectif est de vous montrer la simplicité d’implémentation mais également la simplicité de lecture du code. Dans le sens où ces fonctions n'acceptent que 2 fonctions en paramètres, préférez la version générique offerte par Ramda ou Lodash qui offrent l'avantage d'accepter autant de paramètres que vous le souhaitez.
Les monades les plus couramment utilisées
La monade Either
Prenons par exemple la fonction suivante
const findColorByName = (name) => ({
red: "#ff4444",
blue: "#3b5998",
yellow: "#fff68f",
}[name]);
Vous vous doutez bien que si on appelle cette fonction en lui passant une couleur qui n’existe pas, alors elle renverra “undefined”.
Jusque là, tout va bien - ou presque. Imaginez que plus loin dans le code, vous fassiez un traitement sur la valeur retournée (qui est le code couleur de type string), par exemple, vous exécutez la fonction “toUpperCase()” - et bien si la valeur retournée est bel et bien de type string alors il n’y aura aucun problème - en revanche, si c’est “undefined” qui est retourné, alors vous ne pourrez pas exécuter la fonction “toUpperCase()” au risque de vous prendre une exception qui fera planter votre programme !
Exemple :
findColorByName("yellow").toUpperCase();
// renvoie => #FFF68F
findColorByName("purple").toUpperCase();
// renvoie => Uncaught TypeError: Cannot read property 'toUpperCase' of undefined
Voyons maintenant ce que peut nous apporter l'utilisation d'une monade dans ce genre de cas.
Réécrivons notre fonction “findColorByName” en nous servant d’une monade :
const findColorByName = (name) => {
const found = {
red: "#ff4444",
blue: "#3b5998",
yellow: "#fff68f",
}[name];
return found
? Right(found)
: Left(“Unknown color”);
}
En cas de succès, on renvoie la monade “Right” en lui passant la valeur trouvée, et en cas d’échec, on renvoie la monade “Left” contenant la valeur “null”.
Voyons maintenant l’implémentation des monades “Right” et “Left” :
const Right = x => (
chain: f => f(x),
map: f => Right(f(x)),
fold: (f, g) => g(x),
toString: `Right(${x})`,
});
const Left = x => ({
chain: f => Left(x),
map: f => Left(x),
fold: (f, g) => f(x),
toString: `Left(${x})`,
});
Elles implémentent toutes les deux les fonctions de bases de la monade “Identity” (cf. monade Box créée plus haut).
Vous remarquerez que les fonctions “map” et “toString” sont similaires en tout point. Ne vous inquiétez pas, nous verrons plus tard à quoi sert la fonction “chain”.
Regardons de plus près la fonction “fold” dont le comportement reste inchangé, c’est-à-dire, d’extraire la valeur de sa boite. En revanche, cette fonction prend 2 fonctions en paramètre, notées ici “f” et “g”. Sachez que par convention, c'est la fonction “Right” qui sera appelée en cas de succès et à l’inverse, la fonction “Left” en cas d'échec ou de comportement par défaut (fallback) en cas d'erreur.
Nouveau comportement de la fonction “findColorByName” :
findColorByName("yellow");
// renvoie Right("#fff68f")
et
findColorByName("purple");
// renvoie Left("Unknown color")
Vous évitez ainsi les exceptions et votre programme continue de tourner.
Maintenant, appliquons la fonction "toUpperCase()" avec l’aide de la fonction "map" :
findColorByName("yellow")
.map(x => x.toUpperCase());
// renvoie Right("#FFF68F")
et
findColorByName("purple")
.map(x => x.toUpperCase());
// Toujours Left("Unknown color")
D'après l'implémentation de la monade “Right” (map: fn => Right( fn(x) ) ), la fonction “map” renvoie le résultat de la fonction passée en callback, le tout, encapsulé dans sa boite d'origine, à savoir une monade de type “Right”.
L'avantage, c’est qu'on peut de nouveau chaîner une nouvelle fonction avec l'aide d'une nouvelle fonction “map” (ajout d’un “functor”).
Exemple :
findColorByName("yellow")
.map(x => x.toUpperCase())
.map(x => x.slice(1));
// renvoie Right(FFF68F)
Contrairement à la monade “Right”, l'implémentation de la monade “Left” (map: fn => Left(x)) fait qu’elle ne cherche pas à retourner le résultat de la fonction passée en callback de la fonction “map”, mais tout simplement à retourner la valeur d'origine encapsulée dans une nouvelle boîte de type “Left”. D'où le résultat obtenu => Left(“Unknown color”)
Explication du comportement :
Objectif : éviter au programme de lever une exception et ainsi éviter un crash.
Dans le cas où la fonction “findColorByName” renvoie une monade de type “Right”, alors la callback se trouvant dans la fonction “map” sera exécutée et si elle renvoie une monade de type “Left”, alors la callback se trouvant dans la fonction “map” sera tout simplement ignorée et la monade “Left” sera renvoyée évitant ainsi toute exception obtenue dans la 1ère version de la fonction “findColorByName”.
Améliorations et/ou factorisations possibles de la fonction “findColorByName” :
Créons une nouvelle fonction générique dont la responsabilité sera de retourner une monade “Right” ou “Left” en fonction de la valeur d’une variable :
const fromNullable = (x) => x != null
? Right(x)
: Left(null);
Retravaillons notre fonction “findColorByName” en utilisant notre fonction utilitaire “fromNullable” utilisant des monades "Right" (en cas de succès) et "Left" (en cas d'échec):
const findColorByName = (name) =>
fromNullable({
red: "#ff4444",
blue: "#3b5998",
yellow: "#fff68f",
}[name]);
const getColorSafe = (color) =>
findColorByName(color)
.map(x => x.toUpperCase())
.map(x => x.slice(1))
Exemple d’utilisation
getColorSafe("yellow")
.fold(() > "Unknown color", color => color);
// renvoie => FFF68F
et
getColorSafe("purple")
.fold(() => "Unknown color!", color => color);
// renvoie => Unknown color!
Sympa, non ?
Either : la fonction “chain”
Dans le cas de la monade “Left”, la fonction “chain” adopte le même comportement que la fonction “map” dont l'objectif est d'éviter d'exécuter une fonction qui ferait lever une exception. Elle retourne donc la valeur d'origine encapsulée dans une nouvelle monade de type “Left”.
Pour ce qui est de la monade “Right”, la fonction “chain” à pour objectif d'aplatir les éventuelles encapsulations de monades. Imaginez un cas spécifique dans lequel nous aurions besoin de chaîner plusieurs fonctions dont chacune des fonctions retournent une monade, on se retrouverait avec une encapsulation de monades et l'utilisation de la fonction de debug “toString” nous renverrait, par exemple, la sortie suivante : Right( Right( x ) );
Il convient donc de proposer une solution pour laquelle on pourrait éviter de se retrouver avec une monade contenue dans une autre monade.
Exemple de fonction d'origine :
const getPort = () => {
try {
const str = fs.readFileSync("config.json");
const config = JSON.parse(str);
return config.port;
} catch(e) {
return 8080;
}
}
Dotons nous d'une fonction générique qui nous servira d'utilitaire et utilisant les monades de type "Right" et "Left":
const tryCatch = f => {
try {
return Right(f())
} catch(e) {
return Left(e);
}
};
Créons les fonctions utilisant notre utilitaire “tryCatch” :
const readFileSync = (path) =>
tryCatch(() => fs.readFileSync(path));
const parseJSON = (contents) =>
tryCatch(() => JSON.parse(contents));
Puis créons notre nouvelle fonction “getPortSafe” :
const getPortSafe = () =>
readFileSync("config.json")
.chain(contents => parseJSON(contents))
.map(config => config.port)
.fold(() => 8080, x => x);
console.log("Port:", getPortSafe());
// renvoie => Port: 8080 en cas d'échec ou bien la propriété "port" contenu dans le fichier "config.json"
Notez que la fonction “readFileSync” utilise la fonction utilitaire “tryCatch” qui renvoie soit une monade “Right”, soit une monade “Left” ainsi que la fonction “parseJSON” qui utilise également la fonction utilitaire “tryCatch” et qui renvoie toujours soit une monade “Right”, soit une monade “Left” - il y a donc 3 sorties différentes possibles si nous avions continué d’utiliser la fonction “map” au lieu de “chain” :
- Right(Right(x))
- Right(Left(x))
- Left(x)
Pourquoi 3 et pas 4 ? Il manque :
- Left( Left(x) )
- Left( Right(x) )
Et bien non, souvenez-vous, la monade “Left” renvoie la valeur dans sa boite sans exécuter la fonction de callback passée dans la fonction “map” et/ou “chain” donc le processus de chaînage est interrompu à la 1ère monade “Left” rencontrée.
La monade Maybe
const Nothing = () => ({
map: Nothing,
chain: Nothing,
ap: Nothing,
toString: `Nothing()`,
});
const Just = (v) => ({
map: (fn) => Just(fn(v)),
chain: (fn) => fn(v),
ap: (m) => m.map(v), // m => another Monad
toString: `Just(${JSON.stringify(v)})`,
});
const Maybe = { Just, Nothing, of: Just };
const fromNullable = (val) =>
(val == null)
? Maybe.Nothing()
: Maybe.of(val);
const prop = (prop) => (obj) => fromNullable( obj[prop] );
const someObj = { something: { else: { entirely: 42 } } };
const res = Maybe.of(someObj)
.chain( prop("something") )
.chain( prop("else") )
.chain( prop("entirely") )
.toString;
console.log("res", res);
La monade Task
const Task = (fork) =>
({
fork,
ap: other => Task((rej, res) => fork(rej, f => other.fork(rej, x => res(f(x))))),
map: f => Task((rej, res) => fork(rej, x => res(f(x)))),
chain: f => Task((rej, res) => fork(rej, x => f(x).fork(rej, res))),
concat: (other) => Task((rej, res) => fork(rej, x => other.fork(rej, y => {
console.log('X',x, 'Y', y)
res(x.concat(y))
}))),
fold: (f, g) => Task((rej, res) => fork(x => f(x).fork(rej, res), x => g(x).fork(rej, res)))
});
Exemple d’utilisation de la monade Task
import {Task} from "types";
const t1 = Task((rej, res) => res(2))
.map(two => two + 1)
.map(three => three * 2)
.fork(console.error, console.log);
Exemple concret :
const fs = require('fs');
// Version classique
const app = () =>
fs.readFile('config.json', 'utf-8', (err, contents) => {
if(err) throw err
const newContents = contents.replace(/3/g, '6')
fs.writeFile('config1.json', newContents, (err, _) => {
if(err) throw err
console.log('success!')
})
});
app();
// Version avec monade
const readFile = (path, enc) =>
Task((rej, res) =>
fs.readFile(path, enc, (err, contents) =>
err ? rej(err) : res(contents)
)
);
const writeFile = (path, contents) =>
Task((rej, res) =>
fs.writeFile(path, enc, (err, contents) =>
err ? rej(err) : res(contents)
)
);
const app = () =>
readFile('config.json', 'utf-8')
.map(contents => contents.replace(/3/g, '6'))
.chain(newContents => writeFile('config1.json', newContents));
app()
.fork(console.error, () => console.log("success!"));
Auteur : Gael Cadoret - Architecte IT chez 24s
Sources