Cours JavaScript et TypeScript · Module 12: Programmation fonctionnelle
Currying et application partielle
Dans cette leçon5
Imagine le système d'alimentation des dinosaures du parc. Chaque espèce a ses propres besoins : le type de nourriture, la portion et l'heure du repas. Au lieu de fournir tous les paramètres à chaque fois, on peut préparer des fonctions « préconfigurées » pour une espèce donnée. C'est exactement ce que fait le currying : il transforme une fonction à plusieurs arguments en une séquence de fonctions à un seul argument.
Il y a aussi un second problème, plus technique. Le pipe de la leçon précédente transmet à chaque étape exactement une valeur, donc une fonction qui exige l'espèce, la nourriture et la portion ne s'y emboîte tout simplement pas. Il nous faut un moyen de fixer une partie des arguments à l'avance, pour qu'il ne reste à la fin qu'une fonction à un seul argument.
Qu'est-ce que le currying ?
Le currying est une technique qui transforme une fonction acceptant plusieurs arguments en une séquence de fonctions, dont chacune accepte exactement un argument.
Commençons par une fonction ordinaire à trois paramètres, puis sa version curryfiée écrite de manière classique, avec des function imbriquées :
1// Fonction ordinaire à 3 arguments
2function feedDinosaur(species, food, portion) {
3 return species + ' reçoit ' + portion + 'kg de ' + food;
4}
5feedDinosaur('T-Rex', 'meat', 50);
6
7// Version curryfiée - une séquence de fonctions à un seul argument
8function feedDinosaurCurried(species) {
9 return function(food) {
10 return function(portion) {
11 return species + ' reçoit ' + portion + 'kg de ' + food;
12 };
13 };
14}
15feedDinosaurCurried('T-Rex')('meat')(50);Les deux appels renvoient 'T-Rex reçoit 50kg de meat'. L'appel feedDinosaurCurried('T-Rex') ne nourrit encore personne : il renvoie seulement une fonction qui attend la nourriture, et ce n'est que la troisième paire de parenthèses qui déclenche la vraie logique. Les fonctions internes se souviennent de species et de food grâce aux fermetures, que nous examinerons dans la leçon suivante.
Les fonctions fléchées permettent d'écrire la même chose bien plus brièvement, car chaque flèche correspond à une étape :
1// La même chose avec des flèches (plus concis)
2const feedCurried = (species) => (food) => (portion) =>
3 species + ' reçoit ' + portion + 'kg de ' + food;Lis ce code de gauche à droite : la fonction qui reçoit l'espèce renvoie une fonction qui reçoit la nourriture, et celle-ci renvoie une fonction qui reçoit la portion. Seule l'écriture a changé, le comportement est identique à celui de la version avec function.
Le currying en pratique
La vraie force du currying réside dans la création de fonctions spécialisées. Cette fois, la dernière étape renvoie un objet de repas avec un horodatage :
1const feedCurried = (species) => (food) => (portion) =>
2 ({ species, food, portion, timestamp: Date.now() });
3
4// Fonctions spécialisées pour les espèces
5const feedRex = feedCurried('T-Rex');
6const feedRexMeat = feedRex('meat');
7
8// Utilisation des fonctions spécialisées
9feedRexMeat(50); // { species: 'T-Rex', food: 'meat', portion: 50, ... }
10feedRexMeat(30); // { species: 'T-Rex', food: 'meat', portion: 30, ... }
11
12// Fonctionne aussi pour une autre espèce
13const feedBrachio = feedCurried('Brachiosaurus');
14const feedBrachioPlants = feedBrachio('plants');
15feedBrachioPlants(200); // { species: 'Brachiosaurus', food: 'plants', portion: 200, ... }feedRex et feedRexMeat sont des fonctions ordinaires, que tu peux stocker, transmettre et appeler autant de fois que tu veux, tandis que feedCurried reste générale et s'occupera sans problème du Brachiosaure. L'écriture { species, food, portion } est un raccourci pour { species: species, ... }. Remarque technique : à cause de Date.now(), la dernière étape n'est pas pure, car deux appels avec la même portion donneront des horodatages différents. En revanche, la version sans horodatage, const feed = (species) => (food) => (portion) => ({ species, food, portion }), est bien pure.
Une fonction curry universelle
On peut écrire une fonction curry universelle, qui transforme automatiquement n'importe quelle fonction. La clé, c'est la propriété fn.length, c'est-à-dire le nombre de paramètres déclarés par la fonction (sans le paramètre du reste, et sans les paramètres situés à partir du premier qui a une valeur par défaut) :
1function curry(fn) {
2 return function curried(...args) {
3 if (args.length >= fn.length) {
4 return fn(...args);
5 }
6 return (...moreArgs) => curried(...args, ...moreArgs);
7 };
8}La fonction curried collecte les arguments. Quand il y en a au moins autant que la valeur de fn.length, elle appelle l'original, sinon elle renvoie une fonction qui en ajoutera d'autres. Vérifions-le sur une fonction qui construit un enclos :
1// Utilisation
2function createEnclosure(zone, type, capacity, voltage) {
3 return { zone, type, capacity, voltage };
4}
5
6const curriedCreate = curry(createEnclosure);
7
8// Peut être appelée de plusieurs façons :
9curriedCreate('A')('carnivore')(5)(10000);
10curriedCreate('A', 'carnivore')(5, 10000);
11curriedCreate('A', 'carnivore', 5)(10000);
12curriedCreate('A', 'carnivore', 5, 10000);
13// Tous les appels donnent le même résultatChaque appel renvoie { zone: 'A', type: 'carnivore', capacity: 5, voltage: 10000 }. À strictement parler, la version qui accepte plusieurs arguments à la fois combine le currying et l'application partielle. C'est d'ailleurs ainsi que fonctionnent les fonctions curry des bibliothèques Ramda et lodash. Piège : une fonction déclarée comme (...args) => ... a un length égal à 0, donc curry l'appellera immédiatement.
Application partielle (Partial Application)
L'application partielle est une technique apparentée : elle « gèle » une partie des arguments d'une fonction et crée une nouvelle fonction avec moins de paramètres.
partial accepte une fonction et un nombre quelconque d'arguments de départ (presetArgs), et renvoie une fonction qui attend le reste (laterArgs). L'opérateur spread colle les deux listes en un seul appel :
1function partial(fn, ...presetArgs) {
2 return (...laterArgs) => fn(...presetArgs, ...laterArgs);
3}Appliquons-la à la fonction du journal des incidents, utilisée par toute la sécurité du parc, et construisons à partir d'elle quelques loggers spécialisés :
1// Fonction de journalisation des incidents
2function logIncident(severity, zone, message) {
3 return '[' + severity + '] Zone ' + zone + ': ' + message;
4}
5
6// Loggers spécialisés
7const logCritical = partial(logIncident, 'CRITICAL');
8const logWarning = partial(logIncident, 'WARNING');
9const logCriticalZoneA = partial(logIncident, 'CRITICAL', 'A');
10
11logCritical('B', 'Fence breach detected');
12// '[CRITICAL] Zone B: Fence breach detected'
13
14logCriticalZoneA('T-Rex escaped!');
15// '[CRITICAL] Zone A: T-Rex escaped!'Dans logCritical, un seul argument est gelé, et dans logCriticalZoneA, deux d'un coup. La fonction logIncident d'origine n'a pas changé et accepte toujours trois arguments. Le bind intégré donnera le même effet : logIncident.bind(null, 'CRITICAL').
Currying vs application partielle
Bien que les deux techniques créent des fonctions spécialisées, elles diffèrent dans leur fonctionnement :
- Currying : crée toujours une chaîne de fonctions à un seul argument
- Application partielle : « gèle » un nombre quelconque d'arguments à la fois
La spécialisation par currying passe toujours par les mêmes étapes : tu définis une fonction curryfiée générale, tu fournis le premier argument et tu obtiens une fonction partiellement appliquée, les arguments suivants la restreignent encore, et le dernier renvoie le résultat. Mon conseil : écris les fonctions destinées aux pipelines dans l'ordre « configuration d'abord, données à la fin », par exemple filterBy(key)(value)(arr). Une fois la configuration fournie, il reste une fonction à un seul argument, prête pour pipe, et pour geler ponctuellement quelques arguments d'une fonction existante, partial suffit. Dans le projet final, c'est exactement ainsi que tu construiras les outils de filtrage des relevés.
Retiens : le currying, c'est une station d'alimentation configurée pas à pas - espèce, nourriture, portion - et chaque réglage te donne une nouvelle machine prête à l'emploi.
Code de cette leçon : index.js
1// Currying et application partielle
2console.log("=== Jurassic Park - Traitement par Etapes des Especes ===\n");
3
4// --- EXEMPLE 1 : Currying de base ---
5console.log("--- Currying: Systeme d'alimentation ---");
6
7const feedDinosaur = (species) => (food) => (portion) =>
8 `${species} dostaje ${portion}kg ${food}`;
9
10// Fonctions specialisees
11const feedRex = feedDinosaur("T-Rex");
12const feedRexMeat = feedRex("meat");
13
14console.log(feedRexMeat(50)); // T-Rex dostaje 50kg meat
15console.log(feedRexMeat(30)); // T-Rex dostaje 30kg meat
16
17const feedBrachio = feedDinosaur("Brachiosaurus");
18console.log(feedBrachio("plants")(200)); // Brachiosaurus dostaje 200kg plants
19
20// --- EXEMPLE 2 : Fonction curry universelle ---
21console.log("\n--- curry universel ---");
22
23function curry(fn) {
24 return function curried(...args) {
25 if (args.length >= fn.length) {
26 return fn(...args);
27 }
28 return (...moreArgs) => curried(...args, ...moreArgs);
29 };
30}
31
32function createEnclosure(zone, type, capacity, voltage) {
33 return { zone, type, capacity, voltage };
34}
35
36const curriedCreate = curry(createEnclosure);
37
38// Differentes facons d'appeler
39const r1 = curriedCreate("A")("carnivore")(5)(10000);
40const r2 = curriedCreate("A", "carnivore")(5, 10000);
41const r3 = curriedCreate("A", "carnivore", 5, 10000);
42
43console.log("Appel 1:", JSON.stringify(r1));
44console.log("Appel 2:", JSON.stringify(r2));
45console.log("Identiques ?", JSON.stringify(r1) === JSON.stringify(r2));
46
47// Fabriques specialisees
48const createCarnivoreZone = curriedCreate("X")("carnivore");
49console.log("Zone X-5:", JSON.stringify(createCarnivoreZone(5)(15000)));
50console.log("Zone X-10:", JSON.stringify(createCarnivoreZone(10)(20000)));
51
52// --- EXEMPLE 3 : Application partielle ---
53console.log("\n--- Application partielle ---");
54
55function partial(fn, ...presetArgs) {
56 return (...laterArgs) => fn(...presetArgs, ...laterArgs);
57}
58
59function logIncident(severity, zone, message) {
60 return `[${severity}] Zone ${zone}: ${message}`;
61}
62
63const logCritical = partial(logIncident, "CRITICAL");
64const logWarning = partial(logIncident, "WARNING");
65const logCriticalA = partial(logIncident, "CRITICAL", "A");
66
67console.log(logCritical("B", "Fence breach"));
68console.log(logWarning("A", "Sensor malfunction"));
69console.log(logCriticalA("T-Rex escaped!"));
70
71// --- EXEMPLE 4 : Currying avec map/filter ---
72console.log("\n--- Currying en pratique ---");
73
74const filterByField = curry((field, value, arr) =>
75 arr.filter((item) => item[field] === value)
76);
77
78const dinosaurs = [
79 { name: "Rex", diet: "carnivore", zone: "A" },
80 { name: "Blue", diet: "carnivore", zone: "A" },
81 { name: "Brachio", diet: "herbivore", zone: "B" },
82 { name: "Stego", diet: "herbivore", zone: "B" },
83];
84
85const filterByDiet = filterByField("diet");
86const getCarnivores = filterByDiet("carnivore");
87const getHerbivores = filterByDiet("herbivore");
88
89console.log("Predateurs:", getCarnivores(dinosaurs).map((d) => d.name));
90console.log("Herbivores:", getHerbivores(dinosaurs).map((d) => d.name));Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Qu'est-ce que le currying en programmation fonctionnelle ?
2. En quoi l'application partielle diffère-t-elle du currying ?
Tâches pratiques dans le jeu
- Éditeur de code
Utilise une fonction curry universelle pour créer des filtres et des loggers spécialisés.
- Tri vertical
Ordonne les étapes de la création d'une fonction spécialisée par currying :
- Clique dans l'ordre
Ordonne les éléments d'une fonction fléchée curryfiée :
- Éditeur de code
Utilise partial() pour créer des filtres spécialisés et combine-les en un pipeline avec pipe().