Awesome
Le jargon de la programmation fonctionnelle
Ce document est une traduction française de hemanth/functional-programming-jargon basée sur le commit ce4cc3e.
La programmation fonctionnelle offre de nombreux avantages et sa popularité n’a cessé d’augmenter en conséquence. Cependant, chaque paradigme de programmation défini son propre jargon et la programmation fonctionnelle ne fait pas exception. En fournissant un glossaire, nous espérons faciliter l’apprentissage de la programmation fonctionnelle.
Les exemples sont écrits en JavaScript (ES2015). Pourquoi JavaScript ?
Lorsque cela est possible, ce document utilise les termes définis dans la spécification Fantasy Land.
Traductions
- Portugaise
- Espagnole
- Chinoise
- Indonésienne
- Anglaise (exemples en Python)
- Anglaise (exemples en Scala)
- Anglaise (exemples en Rust)
- Coréenne
- Polonaise
- Turque (exemples en Haskell)
- Russe (exemples en Haskell)
- Anglaise (exemples en Julia)
Table des matières
<!-- RM(noparent,notop) -->- Arité
- Fonction d’ordre supérieur
- Fermeture
- Application partielle
- Curryfication
- Auto-curryfication
- Composition de fonctions
- Continuation
- Fonction pure
- Effets de bord
- Idempotence
- Programmation tacite
- Prédicat
- Contrats
- Catégorie
- Valeur
- Constante
- Foncteur
- Foncteur pointé
- Relèvement
- Transparence référentielle
- Raisonnement équationnel
- Lambda
- Lambda-calcul
- Combinateur fonctionnel
- Évaluation paresseuse
- Monoïde
- Monade
- Comonade
- Composition de Kleisli
- Foncteur applicatif
- Morphisme
- Setoïde
- Demi-groupe
- Foldable
- Lentille
- Signatures de type
- Type algébrique de données
- Option
- Fonction
- Fonction partielle
- Fonction totale
- Bibliothèques de programmation fonctionnelle en JavaScript
Arité
L'arité (arity en anglais) est le nombre d’arguments déclaré par une fonction. On utilise aussi des mots comme unaire, binaire ou ternaire pour désigner le nombre d'arguments.
const somme = (a, b) => a + b // l’arité est 2 (binaire)
const inc = a => a + 1 // l’arité est 1 (unaire)
const zero = () => 0 // l’arité est 0 (nullaires)
Pour aller plus loin
- Arité sur Wikipédia
Fonction d’ordre supérieur
Une fonction d’ordre supérieur (higher-order functions ou HOF en anglais) est une fonction prenant une autre fonction en argument et/ou renvoyant une autre fonction.
const filtre = (predicat, elements) => elements.filter(predicat)
const est = (type) => (element) => Object(element) instanceof type
filtre(est(Number), [0, '1', 2, null]) // [0, 2]
Pour aller plus loin
- Fonction d’ordre supérieur sur Wikipédia
Fermeture
Une fermeture (closure en anglais) est une fonction accompagnée de l’ensemble des variables locales qu’elle a capturées au moment où elle a été définie. Ces variables restent alors accessibles même après que le programme soit sorti du bloc où elles ont été définies.
const ajouterA = x => y => x + y
const ajouterACinq = ajouterA(5)
ajouterACinq(3) // => 8
Dans l’exemple, x
a été capturée par la fermeture ajouterACinq
avec la valeur 5
. ajouterACinq
peut alors être
appelée avec y
pour retourner la somme.
Pour aller plus loin
- Closures (Fermetures) sur MDN
- Lambda Vs Closure (en)
- JavaScript Closures highly voted discussion (en)
Application partielle
L'application partielle d’une fonction (partial application en anglais) est le processus visant à créer une nouvelle fonction en préremplissant certains des arguments de la fonction originale.
// permet de créer des fonctions appliquées partiellement
// prend en paramètre une fonction et des arguments
const appliquerPartiellement = (f, ...args) =>
// retourne une function qui prend en paramètre le reste des arguments
(...resteDesArgs) =>
// et appelle la fonction originale avec tous les arguments
f(...args, ...resteDesArgs)
// une fonction à appliquer partiellement
const ajouter3 = (a, b, c) => a + b + c
// Applique partiellement `2` et `3` à `ajouter3` produit une fonction à un argument
const cinqPlus = appliquerPartiellement(ajouter3, 2, 3) // (c) => 2 + 3 + c
cinqPlus(4) // 9
Il est aussi possible d’utiliser Function.prototype.bind
pour appliquer partiellement une fonction en JavaScript :
const cinqPlus = ajouter3.bind(null, 2, 3) // (c) => 2 + 3 + c
L’application partielle permet de créer des fonctions plus simples à partir de fonctions plus complexes en y intégrant les données au fur et à mesure où elles deviennent disponibles. Les fonctions curryfiées sont automatiquement partiellement appliquées.
Pour aller plus loin
- Partial application (en) sur Wikipédia
Curryfication
La curryfication (currying en anglais) est le processus de conversion d’une fonction qui prend plusieurs arguments en une fonction qui les prend un à la fois.
Chaque fois que la fonction est appelée, elle n’accepte qu’un seul argument et retourne une fonction qui prend un argument jusqu’à ce que tous les arguments soient passés.
const somme = (a, b) => a + b
const sommeCurryfiee = (a) => (b) => a + b
sommeCurryfiee(40)(2) // 42
const ajouterDeux = sommeCurryfiee(2) // (b) => 2 + b
ajouterDeux(10) // 12
Pour aller plus loin
- Curryfication sur Wikipédia
Auto-curryfication
L'auto-curryfication (auto currying en anglais) est la transformation d’une fonction qui prend plusieurs arguments en une fonction qui, si on lui donne moins que son nombre correct d’arguments, renvoie une fonction qui prend le reste des arguments. Lorsque la fonction est appelée avec le nombre correct d’arguments, elle est ensuite évaluée.
Lodash et Ramda ont une fonction curry
qui marche comme suit :
const somme = (x, y) => x + y
const sommeCurryfiee = _.curry(somme)
sommeCurryfiee(1, 2) // 3
sommeCurryfiee(1) // (y) => 1 + y
sommeCurryfiee(1)(2) // 3
Pour aller plus loin
Composition de fonctions
La composition de fonctions (function composition en anglais) est l’acte d'assembler deux fonctions ensemble pour en former une troisième, où la sortie d’une fonction est l’entrée de l’autre. C’est l’une des idées les plus importantes de la programmation fonctionnelle.
const composer = (f, g) => (a) => f(g(a)) // Définition
const arondirEtTransformerEnChaine = composer((val) => val.toString(), Math.floor) // Utilisation
arondirEtTransformerEnChaine(121.212121) // '121'
Pour aller plus loin
- Composition de fonctions sur Wikipédia
Continuation
À tout moment d’un programme, la partie du code qui n’a pas encore été exécutée est appelée continuation.
const afficher = (nombre) => console.log(`Nombre ${nombre}`)
const ajouterUnEtContinuer = (nombre, cc) => {
const resultat = nombre + 1
cc(resultat)
}
ajouterUnEtContinuer(2, afficher) // 'Nombre 3'
Les continuations sont souvent observées en programmation asynchrone lorsque le programme doit attendre de recevoir des données avant de pouvoir continuer à s’exécuter. La réponse est généralement transmise au reste du programme, qui est la continuation, une fois qu’elle a été reçue.
const poursuivreLeProgrammeAvec = (donnees) => {
// Poursuit le programme avec les données en paramètre
}
lireFichierAsync('chemin/vers/fichier', (err, reponse) => {
if (err) {
// prend en charge l’erreur
return
}
poursuivreLeProgrammeAvec(reponse)
})
Pour aller plus loin
- Continuation sur Wikipédia
Fonction pure
Une fonction pure (pure function en anglais) est une fonction :
- dont la valeur de retour n’est déterminée que par ses arguments,
- qui ne produit pas d’effets secondaires.
Une fonction pure doit toujours renvoyer le même résultat lorsqu’elle reçoit les mêmes arguments.
const saluer = (nom) => `Salut, ${nom}`
saluer('Brianne') // renvoie toujour 'Salut, Brianne'
La fonction saluer
suivante n'est pas pure, son résultat dépend de données stockées en dehors de la fonction :
window.name = 'Brianne'
const saluer = () => `Salut, ${window.name}`
saluer() // "Salut, Brianne"
La fonction saluer
suivante n'est pas pure, l’état en dehors de la fonction est modifié.
let salutation
const saluer = (nom) => {
salutation = `Salut, ${nom}`
}
saluer('Brianne')
salutation // "Salut, Brianne"
Pour aller plus loin
- Fonction pure sur Wikipédia
Effets de bord
Une fonction ou une expression est dite à effet de bord (side effects en anglais) si, en plus de renvoyer une valeur, elle interagit (lit ou écrit) avec son environnement externe et que ce dernier est mutable.
const differentAChaqueFois = new Date() // la date change continuellement
console.log('Les entrées-sorties sont un effet de bord !')
Pour aller plus loin
- Effet de bord sur Wikipédia
Idempotence
Une fonction est idempotente (idempotent en anglais) si, quand on l’applique à nouveau à son résultat, elle ne produit pas un résultat différent.
Math.abs(Math.abs(10)) // le résultat sera toujours 10 quelque soit le nombre d'appel à Math.abs
sort(sort(sort([2, 1]))) // une fois la liste triée, le résultat ne change plus
Pour aller plus loin
- Idempotence sur Wikipédia
Programmation tacite
La programmation tacite (tacit programming ou point-free style en anglais) est une manière d’écrire une fonction en n’identifiant pas explicitement les arguments utilisés. Ce style de programmation requiert généralement les notions de curryfication ou d'autres fonctions d’ordre supérieur.
// Étant donné
const map = (fn) => (liste) => liste.map(fn)
const ajouter = (a) => (b) => a + b
// Alors
// Sans la programmation tacite - `nombres` est un argument explicite
const incrementerTout = (nombres) => map(ajouter(1))(nombres)
// Avec la programmation tacite - La liste est un argument implicite
const incrementerTout2 = map(ajouter(1))
Les définitions de fonctions point-free ressemblent à des affectations normales sans function
ou =>
. Il convient
de mentionner que les fonctions point-free ne sont pas nécessairement meilleures que leurs homologues
non-point-free, car elles peuvent être plus difficiles à comprendre lorsqu’elles sont complexes.
Pour aller plus loin
- Tacit programming sur Wikipédia
Prédicat
Un prédicat (predicate en anglais) est une fonction qui retourne vrai ou faux pour une valeur donnée. Une utilisation courante d’un prédicat est la fonction utilisée pour filtrer un tableau.
const predicat = (a) => a > 2
[1, 2, 3, 4].filter(predicat) // [3, 4]
Contrats
Un contrat (contract en anglais) spécifie les obligations et les garanties de comportement qu’une fonction ou une expression doit respecter lors de son exécution. C’est un ensemble de règles qui s’appliquent aux entrées et sorties d’une fonction ou d’une expression. Des erreurs sont levées quand le contrat n’est pas respecté.
// Definie le contrat : int -> boolean
const contrat = (entree) => {
if (typeof entree === 'number') return true
throw new Error('Contrat non respecté : attendu int -> boolean')
}
const ajouterUn = (nombre) => contrat(nombre) && nombre + 1
ajouterUn(2) // 3
ajouterUn('une chaine') // Contrat non respecté : attendu int -> boolean
Catégorie
Une catégorie (category en anglais), dans la théorie des catégories, est un ensemble composé d’objets et des morphismes entre eux. En programmation, les types représentent généralement les objets et les fonctions représentent les morphismes.
Pour qu’une catégorie soit valide, trois règles doivent être respectées :
- Un morphisme d’identité, qui mappe un objet à lui-même, doit exister.
Si
a
est un objet d’une catégorie, alors il doit y avoir une fonction dea -> a
. - Les morphismes doivent pouvoir être composés.
Si
a
,b
, andc
sont des objets d’une catégorie,f
est un morphisme dea -> b
etg
est un morphisme deb -> c
, alorsg(f(x))
doit être équivalent à(g • f)(x)
. - La composition doit être associative.
f • (g • h)
est équivalent à(f • g) • h
Puisque ces règles régissent la composition à un niveau très abstrait, la théorie des catégories est excellente pour découvrir de nouvelles façons de composer des choses.
À titre d’exemple, nous pouvons définir une catégorie Max en tant que classe :
class Max {
constructor (a) {
this.a = a
}
id () {
return this
}
compose (b) {
return this.a > b.a ? this : b
}
toString () {
return `Max(${this.a})`
}
}
new Max(2).compose(new Max(3)).compose(new Max(5)).id().id() // => Max(5)
Pour aller plus loin
- Théorie des catégories sur Wikipédia
- Category Theory for Programmers (en)
Valeur
Une valeur (value en anglais) est tout ce qui peut être assigné à une variable.
5
Object.freeze({ nom: 'John', age: 30 }) // La fonction `freeze` renforce l’immutabilité.
;(a) => a
;[1]
undefined
Constante
Une constante (constant en anglais) est une variable qui ne peut pas être réaffectée une fois définie.
const cinq = 5
const john = Object.freeze({ name: 'John', age: 30 }) // La fonction `freeze` renforce l’immutabilité.
Les constantes sont référentiellement transparentes. Cela veut dire qu’une constante peut être remplacée par la valeur qu’elle représente sans que le résultat du programme soit modifié.
Avec les deux constantes ci-dessus, l’expression suivante renverra toujours true
.
john.age + cinq === ({ name: 'John', age: 30 }).age + 5
Pour aller plus loin
- Constante sur Wikipédia
Fonction constante
Une fonction constante (constant function en anglais) est une fonction curryfiée qui ignore son deuxième argument :
const constante = a => () => a
;[1, 2].map(constante(0)) // => [0, 0]
;[2, 3].map(constante(0)) // => [0, 0]
Foncteur constant
Un foncteur constant (constant functor en anglais) est un object dont la fonction map
ne transforme pas le contenu.
Voir Foncteur.
Constante(1).map(n => n + 1) // => Constante(1)
Monade constante
Une monade constante (constant monad en anglais) est un object dont la fonction chain
ne transforme pas le contenu.
Voir Monade.
Constante(1).chain(n => Constante(n + 1)) // => Constante(1)
Foncteur
Un foncteur (functor en anglais) est un objet qui implémente une fonction map
qui prend en paramètre une fonction
qui sera exécutée sur son contenu. Un foncteur doit respecter deux règles :
Préserver l’identité
objet.map(x => x) // = objet
Être composable
// `f`, `g` sont des fonctions composables arbitraires
objet.map(x => g(f(x))) // = objet.map(f).map(g)
L’implémentation de référence d’Option est un foncteur, en effet :
// préserve l’identité
Some(1).map(x => x) // = Some(1)
// est composable
const f = x => x + 1
const g = x => x * 2
Some(1).map(x => g(f(x))) // = Some(4)
Some(1).map(f).map(g) // = Some(4)
Pour aller plus loin
- Foncteur sur Wikipédia
Foncteur pointé
Un foncteur pointé (pointed functor en anglais) est un objet avec une fonction of
qui stocke n’importe quelle
valeur.
Quand ES2015 a ajouté Array.of
, Array
est devenu un foncteur pointé.
Array.of(1) // [1]
Relèvement
Le relèvement (lifting en anglais) est l’action de prendre une valeur et de la mettre dans un objet tel qu’un foncteur. Si vous relevez une fonction dans un foncteur applicatif, vous pouvez l’appliquer aux valeurs qui se trouvent également dans ce foncteur.
Certaines implémentations ont une fonction appelée lift
ou liftA2
pour faciliter l’exécution de fonctions sur des
foncteurs.
const liftA2 = (f) => (a, b) => a.map(f).ap(b) // note: c’est bien `ap` et non `map`.
const mult = a => b => a * b
const multRelevé = liftA2(mult) // cette nouvelle fonction s’applique à des foncteurs comme Array
multRelevé([1, 2], [3]) // [3, 6]
liftA2(a => b => a + b)([1, 2], [30, 40]) // [31, 41, 32, 42]
Relever une fonction à un argument et l’appliquer fait la même chose que map
.
const incrementer = (x) => x + 1
lift(incrementer)([2]) // [3]
;[2].map(incrementer) // [3]
Relever de simples valeurs peut consister à créer uniquement l’objet.
Array.of(1) // => [1]
Transparence référentielle
Une expression qui peut être remplacée par sa valeur sans changer le comportement du programme est dite référentiellement transparente (referentially transparent en anglais).
Étant donné la fonction saluer
:
const saluer = () => 'Bonjour le monde !'
Toute invocation de saluer()
peut être remplacée par Bonjour le monde !
, donc saluer
est référentiellement
transparente. Cela ne serait pas le cas si saluer
dépendait d’un état externe comme de la configuration ou un appel à
une base de données. Voir aussi fonction pure, raisonnement équationnel.
Pour aller plus loin
- Transparence référentielle sur Wikipédia
Raisonnement équationnel
Lorsqu’une application est composée d’expressions et dépourvue d’effets secondaires, des affirmations peuvent être déduites de ses parties. Vous pouvez également être sûr des détails de votre programme sans avoir à parcourir toutes les fonctions.
const grainesDansChiens = compose(pouletsDansChiens, grainesDansPoulets)
const grainesDansChats = compose(chiensDansChats, grainesDansChiens)
Dans l’exemple ci-dessus, si vous savez que pouletsDansChiens
et grainesDansPoulets
sont pures,
alors vous savez que la composition est également pure. Les choses peuvent être poussées un peu plus loin lorsque l’on
en sait plus sur les fonctions (associativité, commutativité, idempotence, etc...).
Lambda
Une fonction anonyme qui peut être traitée comme une valeur.
;(function (a) {
return a + 1
})
;(a) => a + 1
Les lambdas sont souvent utilisées en tant qu’arguments de fonctions d’ordre supérieur.
;[1, 2].map((a) => a + 1) // [2, 3]
Une lambda peut être affectée à une variable :
const ajouter1 = (a) => a + 1
Lambda-calcul
Une branche des mathématiques qui utilise les fonctions pour créer un modèle universel de calcul.
Combinateur fonctionnel
Un combinateur fonctionnel (functional combinator en anglais) est une fonction d’ordre supérieur, généralement curryfiée, qui renvoie une nouvelle fonction modifiée d’une manière ou d’une autre.
Les combinateurs fonctionnels sont souvent utilisés en programmation tacite pour écrire des programmes particulièrement concis.
// Le combinateur "C" prend une fonction curryfiée à deux arguments et
// en renvoie une nouvelle qui appelle la fonction d’origine avec les arguments inversés.
const C = (f) => (a) => (b) => f(b)(a)
const diviser = (a) => (b) => a / b
const diviserPar = C(diviser)
const diviserPar10 = diviserPar(10)
diviserPar10(30) // => 3
Pour aller plus loin
Évaluation paresseuse
L’évaluation paresseuse (lazy evaluation en anglais) est un mécanisme qui retarde l’évaluation d’une expression jusqu’à ce que sa valeur soit nécessaire. Dans les langages fonctionnels, cela permet des structures telles que les listes infinies, qui ne seraient normalement pas disponibles dans un langage impératif où l’enchaînement des commandes est important.
const rand = function * () {
while (1 < 2) {
yield Math.random()
}
}
const randIter = rand()
randIter.next() // Chaque exécution donne une valeur aléatoire, l’expression est évaluée au besoin.
Pour aller plus loin
- Évaluation paresseuse sur Wikipédia
Monoïde
Un monoïde (monoid en anglais) est un objet avec une fonction qui « combine » cet objet avec un autre du même type (demi-groupe) et qui a de plus une valeur neutre.
Un exemple de monoïde simple est l’addition de nombres :
1 + 1 // 2
Dans ce cas, le nombre est l’objet et +
est la fonction.
Lorsqu’une valeur est combinée avec la valeur neutre, le résultat doit être la valeur d’origine. La fonction doit
également être commutative dans ce cas. La valeur neutre pour l’addition est 0
:
1 + 0 // 1
0 + 1 // 1
1 + 0 === 0 + 1
Il est également nécessaire que le regroupement des opérations n’influe pas sur le résultat (associativité) :
1 + (2 + 3) === (1 + 2) + 3 // true
La concaténation de tableaux forme également un monoïde dont la valeur neutre est un tableau vide ([]
) :
;[1, 2].concat([3, 4]) // [1, 2, 3, 4]
;[1, 2].concat([]) // [1, 2]
À titre de contre-exemple, la soustraction ne forme pas de monoïde car il n’existe pas de valeur neutre commutative :
0 - 4 === 4 - 0 // false
Pour aller plus loin
- Monoïde sur Wikipédia
Monade
Une monade (monad en anglais) est un objet avec les fonctions of
et chain
. La fonction chain
est comme map
sauf qu’elle désimbrique les objets imbriqués résultants.
// Implémentation
Array.prototype.chain = function (f) {
return this.reduce((acc, it) => acc.concat(f(it)), [])
}
// Utilisation
Array.of('chat,chien', 'poisson,oiseau').chain((a) => a.split(',')) // ['chat', 'chien', 'poisson', 'oiseau']
// Contraste avec map
Array.of('chat,chien', 'poisson,oiseau').map((a) => a.split(',')) // [['chat', 'chien'], ['poisson', 'oiseau']]
of
est également connu sous le nom de return
dans d’autres langages fonctionnels. Et chain
est connu sous le nom
de flatmap
ou bind
.
Pour aller plus loin
- Monade sur Wikipédia
Comonade
Une comonade (comonad en anglais) est objet qui a les fonctions extract
et extend
.
const CoIdentité = (v) => ({
val: v,
extract () {
return this.val
},
extend (f) {
return CoIdentité(f(this))
}
})
extract
extrait une valeur d’un foncteur :
CoIdentité(1).extract() // 1
extend
exécute une fonction sur la comonade. La fonction doit renvoyer le même type que la comonade :
CoIdentité(1).extend((co) => co.extract() + 1) // CoIdentité(2)
Pour aller plus loin
- Comonads (en) sur Wikipédia
Composition de Kleisli
Une composition de Kleisli (Kleisli composition en anglais) est une opération pour composer deux fonctions qui
retournent des monades (flèches de Kleisli) quand elles ont des types compatibles. En Haskell, il s’agit de
l’opérateur >=>
.
En JavaScript, en utilisant les options :
// safeParseNum :: String -> Option Number
const safeParseNum = (b) => {
const n = parseNumber(b)
return isNaN(n) ? None() : Some(n)
}
// validatePositive :: Number -> Option Number
const validatePositive = (a) => a > 0 ? Some(a) : None()
// kleisliCompose :: Monad M => ((b -> M c), (a -> M b)) -> a -> M c
const kleisliCompose = (g, f) => (x) => f(x).chain(g)
// parseAndValidate :: String -> Option Number
const parseAndValidate = kleisliCompose(validatePositive, safeParseNum)
parseAndValidate('1') // => Some(1)
parseAndValidate('asdf') // => None
parseAndValidate('999') // => Some(999)
Ceci fonctionne car :
- Option est une monade
validatePositive
etsafeParseNum
renvoient le même type de monade (Option).- Le type d’argument de
validatePositive
correspond au retour non encapsulé desafeParseNum
.
Pour aller plus loin
- Catégorie de Kleisli sur Wikipédia
Foncteur applicatif
Un foncteur applicatif (applicative functor en anglais) est un objet avec une fonction ap
. ap
applique une
fonction de l’objet à une valeur d’un autre objet du même type.
// Implémentation
Array.prototype.ap = function (elements) {
return this.reduce((acc, f) => acc.concat(elements.map(f)), [])
}
// Exemple d’utilisation
;[(a) => a + 1].ap([1]) // [2]
C’est très utile quand vous avez deux objets et que vous souhaitez appliquer une fonction binaire à leurs contenus.
// Tableaux que vous souhaitez fusionner
const arg1 = [1, 3]
const arg2 = [4, 5]
// fonction de fusion - doit être curryfiée pour que cela fonctionne
const ajouter = (x) => (y) => x + y
const ajoutsPartiels = [ajouter].ap(arg1) // [(y) => 1 + y, (y) => 3 + y]
Cela vous donne un tableau de fonctions sur lequel vous pouvez appeler ap
pour obtenir le résultat final :
ajoutsPartiels.ap(arg2) // [5, 6, 7, 8]
Pour aller plus loin
- Applicative functor sur Wikipédia
Morphisme
Un morphisme (morphism en anglais) est une relation entre des objets au sein d’une catégorie. En programmation fonctionnelle, toutes les fonctions sont des morphismes.
Pour aller plus loin
- Morphisme sur Wikipédia
Homomorphisme
Un homomorphisme (homomorphism en anglais) et une fonction où l’entrée et la sortie partagent une même propriété structurelle. Par exemple, dans un homomorphisme monoïde, l’entrée et la sortie sont des monoïdes même si leurs types sont différents.
// convertir :: [number] -> string
const convertir = (a) => a.join(', ')
convertir
est un homomorphisme car :
- array est un monoïde - il possède une opération
concat
et une valeur neutre ([]
), - string est un monoïde - il possède une opération
concat
et une valeur neutre (''
).
Ainsi, un homomorphisme se rapporte à la propriété qui vous intéresse dans l’entrée et la sortie d’une fonction de transformation.
Les endomorphismes et les isomorphismes sont des exemples d’homomorphisme.
Pour aller plus loin
Endomorphisme
Un endomorphisme (endomorphism en anglais) est fonction où le type de l’entrée est identique à celui de sortie.
// majuscule :: String -> String
const enMajuscule = (str) => str.toUpperCase()
// decrementer :: Number -> Number
const decrementer = (x) => x - 1
Pour aller plus loin
- Endomorphisme sur Wikipédia
Isomorphisme
Un isomorphisme (isomorphism en anglais) est un morphisme pour lequel il existe un morphisme inverse. Ce type de morphismes préserve la structure des objets.
Par exemple, les coordonnées 2D peuvent être stockées sous forme de tableau ([2,3]
) ou d’objet ({x: 2, y: 3}
).
// Fonctions de conversions
const paireVersCoords = (paire) => ({ x: paire[0], y: paire[1] })
const coordsVersPaire = (coords) => [coords.x, coords.y]
coordsVersPaire(paireVersCoords([1, 2])) // [1, 2]
paireVersCoords(coordsVersPaire({ x: 1, y: 2 })) // {x: 1, y: 2}
Les isomorphismes sont aussi des homomorphismes puisque les types d’entrée et de sortie sont réversibles.
Pour aller plus loin
- Isomorphisme sur Wikipédia
Catamorphisme
Un catamorphisme (catamorphism en anglais) est ue fonction qui déconstruit une structure en une seule valeur.
reduceRight
est un exemple de catamorphisme pour les structures de type tableau.
// somme est un catamorphisme de [Number] -> Number
const somme = nombres => nombres.reduceRight((accumulateur, nombre) => accumulateur + nombre, 0)
somme([1, 2, 3, 4, 5]) // 15
Pour aller plus loin
- Catamorphisme sur Wikipédia
Anamorphisme
Un anamorphisme (anamorphism en anglais) est une fonction qui construit une structure en appliquant de manière répétée une autre fonction à son argument. C’est le contraire d’un catamorphisme : l'anamorphisme construit une structure et le catamorphisme la décompose.
// génère un tableau à partir d’une fonction et d’une valeur initiale.
const deplier = (f, valeurInitiale) => {
function appliquer (f, valeurInitiale, acc) {
const res = f(valeurInitiale)
return res ? appliquer(f, res[1], acc.concat([res[0]])) : acc
}
return appliquer(f, valeurInitiale, [])
}
const compteARebour = n => deplier((n) => {
return n <= 0 ? undefined : [n, n - 1]
}, n)
compteARebour(5) // [5, 4, 3, 2, 1]
Pour aller plus loin
- Anamorphisme sur Wikipédia
Hylomorphisme
Un hylomorphisme (hylomorphism en anglais) est une fonction récursive composée d’un anamorphisme suivi d’un catamorphisme.
const sommeJusquaX = (x) => somme(compteARebour(x))
sommeJusquaX(5) // 15
Pour aller plus loin
- Hylomorphism (en) sur Wikipédia
Paramorphisme
Un paramorphisme (paramorphism en anglais) est une fonction similaire reduceRight
, bien qu’il y
ait une différence. Dans un paramorphisme, les arguments du réducteur sont la valeur actuelle, la réduction de toutes
les valeurs précédentes et la liste des valeurs qui ont formé cette réduction.
// Dangereux avec des listes contenant `undefined`, mais suffisant pour notre exemple.
const paramorphisme = (reducteur, accumulateur, elements) => {
if (elements.length === 0) { return accumulateur }
const premierElement = elements[0]
const autresElements = elements.slice(1)
return reducteur(premierElement, autresElements, paramorphisme(reducteur, accumulateur, autresElements))
}
const suffixes = liste => paramorphisme(
(x, xs, suffxs) => [xs, ...suffxs],
[],
liste
)
suffixes([1, 2, 3, 4, 5]) // [[2, 3, 4, 5], [3, 4, 5], [4, 5], [5], []]
Le troisième paramètre du réducteur (dans l’exemple ci-dessus, [x, ... xs]
) donne une sorte d’historique de ce qui a
conduit à la valeur accumulateur
actuelle.
Pour aller plus loin
- Paramorphism (en) sur Wikipédia
Apomorphisme
L'apomorphisme (apomorphism en anglais) est le contraire du paramorphisme, tout comme l’anamorphisme est celui du
catamorphisme. Avec le paramorphisme, l’accès à l’accumulateur et à ce qui a été accumulé sont conservés. L’apomorphisme
permet de déplier (unfold
) et laisse la possibilité de retourner une valeur plus tôt.
Pour aller plus loin
- Apomorphism (en) sur Wikipédia
Setoïde
Un setoïde (setoid en anglais) est un objet qui a une fonction equals
qui peut être utilisée pour le comparer à
d’autres objets du même type.
Pour faire de array
un setoïde :
Array.prototype.equals = function (arr) {
const taille = this.length
if (taille !== arr.length) {
return false
}
for (let i = 0; i < taille; i++) {
if (this[i] !== arr[i]) {
return false
}
}
return true
}
;[1, 2].equals([1, 2]) // true
;[1, 2].equals([0]) // false
Demi-groupe
Un demi-groupe (aussi appelé semi-groupe, ou semigroup en anglais), est un objet doté d’une fonction concat
qui
permet de le combiner avec un autre objet du même type.
;[1].concat([2]) // [1, 2]
Pour aller plus loin
- Demi-groupe sur Wikipédia
Foldable
Un foldable (terme anglais) est un objet doté d’une fonction reduce
qui applique une fonction à un accumulateur et à
chaque élément du tableau (de gauche à droite) pour le réduire à une seule valeur.
const somme = (liste) => liste.reduce((acc, val) => acc + val, 0)
somme([1, 2, 3]) // 6
Lentille
Une lentille (lens en anglais) est une structure (souvent un objet ou une fonction) qui associe un getter et un setter non mutable et est utilisé sur d’autres structures.
// En utilisant les [lentilles de Ramda (en)](https://ramdajs.com/docs/#lens)
const lentilleSurLeNom = R.lens(
// getter pour la propriété nom
(obj) => obj.nom,
// setter pour la propriété nom
(val, obj) => Object.assign({}, obj, { nom: val })
)
Le fait d’avoir des accesseurs pour une structure donnée permet quelques fonctionnalités clés.
const personne = { nom: 'Gertrude Blanch' }
// utilisation du getter
R.view(lentilleNom, personne) // 'Gertrude Blanch'
// utilisation du setter
R.set(lentilleNom, 'Shafi Goldwasser', personne) // {nom: 'Shafi Goldwasser'}
// applique la fonction `majuscule` sur la valeur de la structure
R.over(lentilleNom, majuscule, personne) // {nom: 'GERTRUDE BLANCH'}
Les lentilles sont aussi composables. Cela permet d’effectuer facilement des mises à jour de données profondément imbriquées.
// Cette lentille se concentre sur le premier élément d’un tableau non vide
const lentillePremierElement = R.lens(
// getter sur le premier élément du tableau
elements => elements[0],
// setter non mutable du premier élément du tableau
(val, [__, ...elements]) => [val, ...elements]
)
const personnes = [{ nom: 'Gertrude Blanch' }, { nom: 'Shafi Goldwasser' }]
// Malgré ce que vous pourriez supposer, les lentilles se composent de gauche à droite.
R.over(compose(lentillePremierElement, lentilleNom), majuscule, personnes) // [{'nom': 'GERTRUDE BLANCH'}, {'nom': 'Shafi Goldwasser'}]
Autres implémentations :
Signatures de type
Souvent, les fonctions en JavaScript incluent des commentaires qui indiquent les types de leurs arguments et les valeurs de retour. C'est leur signature de type (type signature en anglais).
Les pratiques varient beaucoup au sein de la communauté, mais ils suivent souvent les modèles suivants :
// nomFonction :: premierType -> secondType -> typeDeRetour
// ajouter :: Number -> Number -> Number
const ajouter = (x) => (y) => x + y
// incrementer :: Number -> Number
const incrementer = (x) => x + 1
Si une fonction accepte une autre fonction comme argument, elle est placée entre parenthèses :
// appeler :: (a -> b) -> a -> b
const appeler = (f) => (x) => f(x)
Les lettres a
, b
, c
, d
sont utilisée pour signifier que l’argument peut être de n’importe quel type. La version
suivante de map
prend une fonction qui transforme une valeur d’un type a
en un autre type b
, un tableau de valeurs
de type a
, et retourne un tableau de valeurs de type b
.
// map :: (a -> b) -> [a] -> [b]
const map = (f) => (liste) => liste.map(f)
Pour aller plus loin
- Signature de type sur Wikipédia
- Ramda’s type signatures (en)
- Mostly Adequate Guide (en)
- What is Hindley-Milner? (en) sur Stack Overflow
Type algébrique de données
Un type algébrique de données (algebraic data type en anglais) est un type composite fabriqué à partir de l’assemblage d’autres types. Deux classes communes de types algébriques sont les sommes et les produits.
Pour aller plus loin
- Type algébrique de données sur Wikipédia
Types somme
Un type somme (sum type en anglais) est l’union de deux types l’un avec l’autre. On l’appelle ainsi car le nombre de valeurs possibles dans le type résultant est la somme des types d’entrée.
JavaScript n’a pas de type somme, mais on peut néanmoins utiliser les Set
s le simuler :
// Imaginez que plutôt que des ensembles ici, nous avons des types qui ne peuvent prendre que ces valeurs.
const bools = new Set([true, false])
const halfTrue = new Set(['half-true'])
// Le type LogiqueFaible contient la somme des valeurs de bools et halfTrue
const valeursDeLogiqueFaible = new Set([...bools, ...halfTrue])
Les types somme sont parfois appelés types union (union types en anglais), unions discriminées (discriminated unions en anglais) ou unions étiquetées (tagged unions en anglais).
Il existe plusieurs bibliothèques en JavaScript qui facilitent la définition et l’utilisation des types somme.
Flow défini des types union et TypeScript des énumérations pour jouer ce rôle.
Pour aller plus loin
- Type somme sur Wikipédia
Types produit
Un type produit (product type en anglais) combine des types d’une manière que vous connaissez probablement mieux :
// point :: (Number, Number) -> {x: Number, y: Number}
const point = (x, y) => ({ x, y })
L’exemple précédent est ce qu’on appelle un produit car l’ensemble des valeurs possibles de la structure de données est le produit des différentes valeurs. De nombreux languages ont un type tuple qui est la formulation la plus simple d’un type produit.
Pour aller plus loin
- Type produit sur Wikipédia
- Théorie des ensembles sur Wikipédia
Option
L’option (terme identique en anglais) est un type somme qui encapsule deux cas souvent appelés
Some
et None
. Les options sont utiles avec des fonctions qui peuvent ne pas renvoyer de valeur.
// Définition naïve
const Some = (v) => ({
val: v,
map (f) {
return Some(f(this.val))
},
chain (f) {
return f(this.val)
}
})
const None = () => ({
map (f) {
return this
},
chain (f) {
return this
}
})
// optionnel :: (String, {a}) -> Option a
const optionnel = (key, obj) => typeof obj[key] === 'undefined' ? None() : Some(obj[key])
Utilisez chain
pour enchaîner des fonctions qui retournent des Option
s:
// getArticle :: Panier -> Option Article
const getArticle = (panier) => optionnel('article', panier)
// getPrix :: Article -> Option Number
const getPrix = (article) => optionnel('prix', article)
// getPrixImbrique :: cart -> Option a
const getPrixImbrique = (panier) => getArticle(panier).chain(getPrix)
getPrixImbrique({}) // None()
getPrixImbrique({ item: { foo: 1 } }) // None()
getPrixImbrique({ item: { price: 9.99 } }) // Some(9.99)
Option
est aussi connu sous le nom Maybe
. Some
est parfois appelé Just
, et None
est parfois appelé Nothing
.
Fonction
En programmation fonctionnelle, une fonction (ou expression lambda) f :: A => B
est une expression avec un seul
argument immuable de type A
et une seule valeur de retour de type B
. La valeur de retour dépend entièrement de
l’argument, ce qui rend la fonction référentiellement transparente. Cela implique donc
que la fonction ne produit aucun effet de bord caché et qu'elle est pure par
définition. Ces propriétés rendent les fonctions agréables à manipuler : elles sont complètement déterministes et ainsi
prévisibles. Les fonctions permettent d’utiliser le code comme des données, en faisant abstraction du comportement :
// fois2 :: Number -> Number
const fois2 = n => n * 2
;[1, 2, 3].map(fois2) // [2, 4, 6]
Fonction partielle
Une fonction partielle est une function qui n’est pas définie pour tous les arguments - elle peut renvoyer un résultat inattendu ou ne jamais se terminer. Les fonctions partielles ajoutent de la surcharge cognitive, elles sont plus difficiles à appréhender et peuvent entraîner des erreurs d’exécution.
// exemple 1 : somme d’une liste
// somme :: [Number] -> Number
const somme = nombres => nombres.reduce((a, b) => a + b)
somme([1, 2, 3]) // 6
somme([]) // TypeError: Reduce of empty array with no initial value
// exemple 2 : récupération du premier élément d’une liste
// premier :: [A] -> A
const premier = a => a[0]
premier([42]) // 42
premier([]) // undefined
// ou pire
premier([[42]])[0] // 42
premier([])[0] // Uncaught TypeError: Cannot read property '0' of undefined
// exemple 3 : repéter une fonction N fois
// repeter :: Number -> (Number -> Number) -> Number
const repeter = n => fn => n && (fn(n), times(n - 1)(fn))
repeter(3)(console.log)
// 3
// 2
// 1
repeter(-1)(console.log) // RangeError: Maximum call stack size exceeded
Manipuler des fonctions partielles
Les fonctions partielles sont dangereuses et doivent être utilisées avec prudence. Vous pouvez en effet obtenir un résultat inattendu (erroné), rencontrer des erreurs d’exécution, ou même s’exécuter indéfiniment lorsque vous les utilisez. Connaitre et traiter tous ces cas aux limites peut donc devenir très fastidieux.
Heureusement, il est possible de transformer une fonction partielle en fonction traditionnelle (ou fonction totale).
On peut en effet utiliser des valeurs par défaut ou des garde-fous pour traiter les entrées pour lesquelles la fonction
partielle n’a pas de comportement défini. En utilisant par exemple le type Option, nous pouvons obtenir
soit Some(value)
ou None
là où la fonction se serait comportée de manière inattendue.
// exemple 1: somme d’une liste
// on peut fournir une valeur par défaut afin que la fonction renvoie toujours un résultat
// somme :: [Number] -> Number
const somme = arr => arr.reduce((a, b) => a + b, 0)
somme([1, 2, 3]) // 6
somme([]) // 0
// exemple 2: récupération du premier élément d’une liste
// on peut utiliser le type Option
// premier :: [A] -> Option A
const premier = a => a.length ? Some(a[0]) : None()
premier([42]).map(a => console.log(a)) // 42
premier([]).map(a => console.log(a)) // console.log ne s’exécutera pas
// et avec le pire scénario
premier([[42]]).map(a => console.log(a[0])) // 42
premier([]).map(a => console.log(a[0])) // console.log ne s’exécutera pas, et il n’y aura pas d’erreur
// De plus, vous saurez grâce au type de retour (Option) que vous devez utiliser `.map`
// pour accèder à la valeur de retour et vous n’oublierez plus de vérifier l’argument
// car cela devient obligatoire.
// exemple 3: repéter une fonction N fois
// on fait en sorte que la fonction se termine toujours en changeant les conditions
// repeter :: Number -> (Number -> Number) -> Number
const repeter = n => fn => n > 0 && (fn(n), times(n - 1)(fn))
repeter(3)(console.log)
// 3
// 2
// 1
repeter(-1)(console.log) // ne fera rien
En rendant vos fonctions partielles totales, les erreurs d’exécution peuvent être évités. Toujours renvoyer une valeur permet de plus d’obtenir du code qui est à la fois plus facile à comprendre et à maintenir.
Fonction totale
Une fonction partielle est une function qui renvoie un résultat valide pour toutes les valeurs d’entrées possibles. Les fonctions totales s’opposent aux fonctions partielles qui peuvent générer des erreurs, renvoyer un résultat inattendu ou s’exécuter indéfiniment.
Bibliothèques de programmation fonctionnelle en JavaScript
- mori
- Immutable
- Immer
- Ramda
- ramda-adjunct
- ramda-extension
- Folktale
- monet.js
- lodash
- Underscore.js
- Lazy.js
- maryamyriameliamurphies.js
- Haskell in ES6
- Sanctuary
- Crocks
- Fluture
- fp-ts
P.S. : Merci à toutes les personnes ayant contribué au dépôt d’origine comme à ce dépôt !