MonetJS - Identity Monad
Je trouve Scala de plus en plus intĂ©ressant, et certains concepts fonctionnels me manque en JavaScript. Jâai donc essayĂ© divers frameworks JavaScript dits âfonctionnelâ et jâen ai enfin trouvĂ© un que je trouve simple Ă assimiler et Ă utiliser: monet.js. Donc, aujourdâhui nous parlon de la monad(e) Identity
Rappel: Monad?
â ïž Disclaimer: ma dĂ©finition ne va pas plaire Ă tout le monde, mais câest un autre dĂ©bat (ma mission câest de montrer que le fonctionnel câest pas compliquĂ© et que ça peut servir.)
Pour faire court: une monad(e) est un container qui contient une valeur que lâon ne peut pas modifier (vous pouvez replacer âimmutabilitĂ©â dans une conversation). Un peu comme ceci:
function monad(v) {
let value = v
this.get = () => value
return this
}
et je peux lâutiliser comme ceci:
let m = new monad(42)
console.log("value of m", m.get())
⊠et je ne pourrais JAMAIS modifier value
⊠mais en fait ce nâest pas une monade đČ
Pour que ce soit une monade,
il faut lui ajouter une méthode map
qui permet dâappliquer Ă la valeur du container une opĂ©ration (fonction/closure) retournant elle mĂȘme une valeur et dâobtenir enfin une nouvelle monade sans changer la valeur de la prĂ©cĂ©dente đ:
function monad(v) {
let value = v
this.get = () => value
this.map = (operation) => new this.constructor(operation(value))
return this
}
Et on lâutilisera comme cela:
let m = new monad(40)
let m2 = m.map(n => n + 2)
m.get() == 40
m2.get() == 42
Jâai donc obtenu une nouvelle monade avec une valeur de 42
et ma 1Ăšre monade reste avec une valeur de 40
.
⊠Mais, mais, mais đŠ âŠ. Ce nâest pas encore une monade, câest uniquement un Functor
Il faut encore lui ajouter une méthode flatMap
, câest un peu la mĂȘme chose que pour map
, mais dans ce cas lĂ lâopĂ©ration appliquĂ©e ne retourne pas une valeur âsimpleâ mais aussi une monade (ou Ă©ventuellement un functor ou un container ⊠mais ça peut se dicuter). Et du coup il faut âapplatirâ la monade retournĂ©e par lâopĂ©ration pour Ă©viter dâimbriquer des monades dans des monades (oui je sais đ).
Câest Ă dire,
- que lâon applique lâopĂ©ration sur la valeur de la monade
- on en récupÚre la nouvelle valeur
- on retourne une nouvelle monade avec la nouvelle valeur
En code, câest peut ĂȘtre un peu plus clair:
function monad(v) {
let value = v
this.get = () => value
this.map = (operation) => new this.constructor(operation(value))
this.flatMap = (operation) => new this.constructor(operation(value).get())
return this
}
Et on lâutilisera comme cela:
let m = new monad(40)
let add1 = n => new monad(n+1)
m.flatMap(add1).flatMap(add1).get() == 42
Et pour mieux comprendre, vous nâavez quâĂ essayer de faire:
m.map(add1).map(add1).get()
Maintenant passons Ă monet.js.
Identity
Tout ce que je viens dâexpliquer est dĂ©jĂ implĂ©mentĂ© dans monet.js sous le nom de monad Identity.
â ïž vous pouvez utiliser monet.js dans votre browser ou avec node.js
Utilisation
Câest simple:
const monet = require('monet');
let monadWith40 = monet.Identity(40) // number 40 wrapped in an Identity box
let addOne = n => n +1
monadWith40.map(addOne).map(addOne).get() == 42
Et dans la vraie vie, est-ce que cela peut servir?
Alors, la monade Identity nâest pas forcĂ©ment la monade la plus âš , mais oui elle peut servir.
Imaginons que vous ayez toutes les Ă©tapes dâun projet modĂ©lisĂ©es sous forme de fonctions comme ceci:
let coding = p => ({total: p.total + p.dev, dev: p.dev})
/* on pourrait aussi Ă©crire:
function coding(p) {
return {
total: p.total + p.dev,
dev: p.dev
}
}
*/
let specifications = p => ({total: p.total + p.dev * 8.72 / 100, dev: p.dev})
let architecture = p => ({total: p.total + p.dev * 16.46 / 100, dev: p.dev})
let integrationAndTests = p => ({total: p.total + p.dev * 15.48 / 100, dev: p.dev})
let buildingAndPackaging = p => ({total: p.total + p.dev * 15.48 / 100, dev: p.dev})
let documentation = p => ({total: p.total + p.dev * 7.25 / 100, dev: p.dev})
let environmentLogistics = p => ({total: p.total + p.dev * 13.51 / 100, dev: p.dev})
let managementAndQuality = p => ({total: p.total + p.dev * 28.13 / 100, dev: p.dev})
Donc si je souhaite par exemple, connaĂźtre le nombre de jours de spĂ©cifications pour un projet oĂč le dev fait 100 jours, je ferais ceci:
specifications({total:0,dev:100}).total == 8.72
si jâutilise monet.js, je âchiffreraisâ mon projet comme ceci:
let startEstimation = monet.Identity({total:0, dev:100})
let globalEstimation =
startEstimation
.map(coding)
.map(specifications)
.map(architecture)
.map(integrationAndTests)
.map(buildingAndPackaging)
.map(documentation)
.map(environmentLogistics)
.map(managementAndQuality)
console.log("Total:", globalEstimation.get()) // 205.02999999999997
Si jâai besoin de nâavoir que la charge de documentation:
startEstimation.map(documentation).get()
Il y a aussi du flatMap
Je rappelle que câest une monade đ
On imagine par exemple que le chef de projet veut se faire une petite provision pour risque de 5 jours et quâil lâimplĂ©mente de cette façon:
let provision = p => monet.Identity({total: p.total + 5, dev: p.dev})
Comme cette fois-ci mon âĂ©tapeâ provision
me retourne une monade, je vais utiliser flatMap
pour lâintĂ©grer dans mon chiffrage:
let globalEstimation =
startEstimation
.map(coding)
.flatMap(provision) // <-- c'est ici
.map(specifications)
.map(architecture)
.map(integrationAndTests)
.map(buildingAndPackaging)
.map(documentation)
.map(environmentLogistics)
.map(managementAndQuality)
VoilĂ , ce nâest pas plus compliquĂ© que ça.
PS: en ce qui concerne
flatMap
vous pouvez en entendre aussi parler sous le nom debind
oud defmap
.
A bientĂŽt pour la suite avec Maybe
, Some
et None