Programmation fonctionnelle en JavaScript : de la théorie à la pratique sur Node.js
La programmation fonctionnelle (PF) est un paradigme qui considère les calculs comme l'évaluation de fonctions mathématiques et évite les changements d'état et les données mutables. Dans le contexte de JavaScript et Node.js, la PF permet d'écrire un code plus prévisible, testable et facile à maintenir, en particulier lors du travail avec des flux asynchrones et des transformations de données complexes. Cet article est destiné aux développeurs déjà familiers avec les bases de la PF et approfondit les concepts avancés : composition de fonctions, foncteurs, monades et gestion des effets de bord.
1. Fonctions pures et immutabilité : le fondement de la fiabilité
À un niveau avancé, il ne suffit pas de connaître la définition d'une fonction pure. Il est important de comprendre comment l'immutabilité et l'absence d'effets de bord influencent l'architecture d'une application, en particulier dans l'environnement Node.js où les opérations concurrentes sont fréquentes.
1.1. Copie profonde et partage structurel (Structural Sharing)
La copie profonde complète de grands objets peut être coûteuse. Au lieu de cela, des bibliothèques comme Immer ou Immutable.js utilisent le partage structurel : lors de la modification d'une partie d'un objet, une nouvelle référence est créée, mais les parties inchangées sont partagées avec l'état précédent.
// Exemple avec Immer (bibliothèque populaire)const { produce } = require('immer');
const baseState = { user: { name: 'Alice', age: 30 }, meta: { created: Date.now() }};
// produce crée un nouvel état sans muter baseStateconst nextState = produce(baseState, draft => { draft.user.age = 31;});
console.log(baseState.user.age); // 30console.log(nextState.user.age); // 31console.log(baseState.meta === nextState.meta); // true (Structural Sharing)1.2. Fonctions pures et mémoïsation
Les fonctions pures sont idéales pour la mémoïsation — la mise en cache des résultats pour éviter des calculs répétés. Dans Node.js, c'est crucial pour les calculs lourds ou les requêtes à la base de données.
// Mémoïsation simple pour une fonction pureconst memoize = (fn) => { const cache = new Map(); return (...args) => { const key = JSON.stringify(args); if (cache.has(key)) { console.log('Depuis le cache'); return cache.get(key); } const result = fn(...args); cache.set(key, result); return result; };};
const fibonacci = (n) => (n <= 1 ? n : fibonacci(n - 1) + fibonacci(n - 2));const memoizedFib = memoize(fibonacci);
console.log(memoizedFib(40)); // Calculéconsole.log(memoizedFib(40)); // Depuis le cache2. Composition de fonctions : construire du complexe à partir du simple
La composition est le processus qui consiste à combiner deux ou plusieurs fonctions pour en obtenir une nouvelle. À un niveau avancé, nous utilisons non seulement la composition simple, mais aussi des motifs comme Pipe et Compose, qui sont la base pour construire des pipelines de traitement de données.
2.1. Pipe et Compose
Pipe exécute les fonctions de gauche à droite, tandis que Compose les exécute de droite à gauche. Dans Node.js, cela est souvent utilisé pour traiter les données entrantes (par exemple, les requêtes HTTP).
// Implémentation de pipeconst pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);
// Exemple : traitement d'une chaîneconst toLowerCase = (str) => str.toLowerCase();const removeSpaces = (str) => str.replace(/\\s+/g, '-');const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
const slugify = pipe(toLowerCase, removeSpaces, capitalize);console.log(slugify('Hello World From Node.js')); // Hello-world-from-node.js2.2. Pipelines asynchrones
Dans les applications Node.js réelles, les fonctions sont souvent asynchrones. Créons une version de pipe pour travailler avec des promesses.
const asyncPipe = (...fns) => (x) => fns.reduce((acc, fn) => acc.then(fn), Pr