Kodokon kodokon.com

Patterns fonctionnels : currying, composition, immutabilité

Structurez votre code avec le currying, la composition de fonctions et des données immuables pour gagner en prévisibilité et en testabilité.

9 min · 3 questions

Ouvrir cette leçon dans Kodokon

JavaScript traite les fonctions comme des valeurs de première classe, ce qui ouvre la voie aux patterns fonctionnels. Le currying transforme une fonction à n arguments en une chaîne de fonctions à un argument : f(a, b, c) devient f(a)(b)(c). À ne pas confondre avec l'application partielle, qui fixe certains arguments en une seule fois (fn.bind(null, a)). Le currying brille pour créer des fonctions spécialisées à partir de fonctions génériques.

JAVASCRIPT
const curry = (fn) => {
  const arity = fn.length;
  return function curried(...args) {
    if (args.length >= arity) return fn(...args);
    return (...rest) => curried(...args, ...rest);
  };
};

const add3 = (a, b, c) => a + b + c;
const addCurried = curry(add3);

console.log(addCurried(1)(2)(3)); // 6
console.log(addCurried(1, 2)(3)); // 6
Curry générique basé sur l'arité de la fonction.

Subtilité de spécification : fn.length ne compte que les paramètres situés avant le premier paramètre par défaut ou rest. (a, b = 1) => {} a une length de 1, (...args) => {} de 0. Un curry fondé sur l'arité doit donc recevoir des fonctions aux paramètres simples. La composition est l'étape suivante : assembler des petites fonctions pures en pipelines lisibles.

JAVASCRIPT
const pipe = (...fns) => (input) =>
  fns.reduce((acc, fn) => fn(acc), input);

const trim = (s) => s.trim();
const lower = (s) => s.toLowerCase();
const dashes = (s) => s.replace(/\s+/g, "-");

const toSlug = pipe(trim, lower, dashes);
console.log(toSlug("  Hello World  "));
// "hello-world"
pipe applique les fonctions de gauche à droite.

pipe(f, g, h)(x) calcule h(g(f(x))) : lecture dans l'ordre d'exécution. La variante compose applique de droite à gauche, comme la notation mathématique. Ces pipelines n'ont de valeur que si les fonctions sont pures - sans effet de bord ni dépendance cachée - ce qui suppose des données immuables : au lieu de modifier un objet, vous en produisez une nouvelle version, et l'égalité de référence suffit alors à détecter un changement (le cœur de la réconciliation de React).

JAVASCRIPT
const state = Object.freeze({
  user: { name: "Ada" },
  tags: ["js"]
});

const next = {
  ...state,
  tags: [...state.tags, "fp"]
};

const deepCopy = structuredClone(state);
deepCopy.user.name = "Grace"; // copie independante
Nouvelle version par copie plutôt que mutation.

Quiz de validation

Vérifiez que vous avez bien retenu les points clés de cette leçon.

  1. Quelle est la différence entre currying et application partielle ?
    • Aucune, ce sont deux synonymes
    • Le currying transforme f(a, b, c) en f(a)(b)(c) ; l'application partielle fixe certains arguments en un seul appel
    • L'application partielle ne fonctionne qu'avec bind, le currying qu'avec les fléchées
    • Le currying ne s'applique qu'aux fonctions à deux arguments
  2. À quoi équivaut pipe(f, g, h)(x) ?
    • f(g(h(x)))
    • h(g(f(x)))
    • g(f(h(x)))
    • f(x) + g(x) + h(x)
  3. Après const s = Object.freeze({ a: { b: 1 } }), l'affectation s.a.b = 2 fonctionne-t-elle ?
    • Non, freeze gèle tout l'arbre d'objets
    • Non, une TypeError est levée même hors mode strict
    • Oui, car freeze est superficiel : l'objet imbriqué s.a n'est pas gelé
    • Oui, mais uniquement en mode non strict