La composition de fonctions est un concept fondamental de la programmation fonctionnelle, largement popularisé dans l'écosystème JavaScript par des bibliothèques comme Redux. Elle permet de combiner plusieurs fonctions pour créer une nouvelle fonction où le résultat de l'une devient l'argument de la suivante.
Logique de fonctionnement
L'objectif de la fonction compose est d'exécuter une séquence de fonctions de droite à gauche :
- La dernière fonction de la liste peut accepter plusieurs paramètres.
- Toutes les autres fonctions doivent être unaires (n'accepter qu'un seul argument, qui est le retour de la fonction précédente).
- Le résultat final est le produit de cette chaîne d'appels successifs.
Approche fonctionnelle avec reduce
L'implémentation la plus élégante et la plus concise utilise la méthode Array.prototype.reduce. C'est d'ailleurs l'approche retenue dans le code source de Redux.
/**
* Combine plusieurs fonctions de droite à gauche.
* @param {...Function} fns Les fonctions à composer.
*/
function compose(...fns) {
if (fns.length === 0) {
return (val) => val;
}
if (fns.length === 1) {
return fns[0];
}
return fns.reduce((acc, curr) => (...args) => acc(curr(...args)));
}
Analysons l'exécution de compose(f3, f2, f1)(x, y) :
- Le premier passage de
reduceproduit une fonction intermédiaire :(...args) => f3(f2(...args)). - Le second passsage intègre
f1, résultant en :(...args) => f3(f2(f1(...args))). - Lors de l'appel final,
f1reçoit les arguments initiaux, puis son résultat remonte la chaîne.
const additionner = (a, b) => a + b;
const auCarre = (n) => n * n;
const doubler = (n) => n * 2;
const calculCombine = compose(doubler, auCarre, additionner);
// (2 + 3) = 5 -> 5^2 = 25 -> 25 * 2 = 50
console.log(calculCombine(2, 3)); // 50
Approche itérative
Pour des raisons de performance ou de clarté algorithmique, on peut également implémenter compose via une boucle traditionnelle. Cette méthode gère manuellement le passage des résultats à travers les indices du tableau de foncitons.
function composeIterative(...operators) {
const count = operators.length;
return function(...initialArgs) {
if (count === 0) return initialArgs;
// On commence par la fonction la plus à droite
let cursor = count - 1;
let result = operators[cursor].apply(this, initialArgs);
// On remonte vers la gauche
while (cursor--) {
result = operators[cursor].call(this, result);
}
return result;
};
}
Dans cette version, nous utilsions apply pour l'appel initial afin de supporter une arité multiple, puis call pour les étapes suivantes puisque chaque fonction intermédiaire ne renvoie qu'une seule valeur. Si l'ordre d'exécution doit être inversé (de gauche à droite), on utilise généralement le terme pipe au lieu de compose.