Cours JavaScript et TypeScript · Module 12: Programmation fonctionnelle

Qu'est-ce que la programmation fonctionnelle ?

8 min de lecture
Dans cette leçon6

Le rapport du matin sur l'enclos des raptors indique : 12 individus. Celui du soir, généré par la même fonction à partir des mêmes données : 13. Rien n'a éclos, rien ne s'est échappé - une fonction a discrètement modifié un compteur partagé. Ce genre de bug ne se reproduit pas sur commande, et la programmation fonctionnelle lui coupe l'herbe sous le pied.

Imagine un laboratoire de Jurassic Park où chaque expérience sur l'ADN d'un dinosaure donne toujours le même résultat pour les mêmes données d'entrée. Il n'y a aucun effet de bord - aucun dinosaure ne s'échappe, aucun système de sécurité n'est désactivé par accident. C'est exactement ainsi que fonctionne la programmation fonctionnelle (PF) : c'est un paradigme dans lequel le code se comporte comme une expérience scientifique contrôlée.

Ce que tu vas apprendre

  • distinguer les fonctions pures des fonctions impures,
  • traiter des tableaux d'échantillons avec map, filter et reduce,
  • assembler des fonctions en chaînes de traitement avec pipe et compose,
  • configurer des fonctions par étapes et enfermer des données dans des fermetures,
  • parcourir l'arbre des espèces par récursivité et modifier des données sans mutation,
  • gérer les données manquantes et les erreurs grâce à Maybe et Either,
  • construire un système fonctionnel de rapports de sécurité du parc.

Paradigme fonctionnel vs impératif

En programmation impérative, on dit à l'ordinateur comment faire quelque chose, étape par étape. En programmation fonctionnelle, on décrit ce que l'on veut obtenir, en utilisant les fonctions comme briques de base.

La tâche : dans une liste d'espèces, on sélectionne les prédateurs. D'abord une boucle classique, puis la version fonctionnelle :

1// Approche impérative - étape par étape
2const dinosaurs = ['T-Rex', 'Velociraptor', 'Triceratops', 'Stegosaurus'];
3const carnivores = [];
4for (let i = 0; i < dinosaurs.length; i++) {
5  if (dinosaurs[i] === 'T-Rex' || dinosaurs[i] === 'Velociraptor') {
6    carnivores.push(dinosaurs[i]);
7  }
8}
9
10// Approche fonctionnelle - déclarative
11const isCarnivore = (name) => ['T-Rex', 'Velociraptor'].includes(name);
12const carnivoresFP = dinosaurs.filter(isCarnivore);

Les deux versions donnent ['T-Rex', 'Velociraptor']. Dans la première, c'est nous qui gérons le compteur i, la condition de la boucle et l'ajout des résultats avec push. Dans la seconde, on se contente de nommer la règle : isCarnivore vérifie avec la méthode includes si le nom figure sur la liste des prédateurs, et le parcours du tableau est confié à la méthode filter, qui renvoie un nouveau tableau contenant les éléments correspondants. Qu'est-ce qui n'a pas changé ? Dans les deux versions, le tableau dinosaurs reste intact.

Fonctions pures (Pure Functions)

Une fonction pure est une expérience contrôlée : pour les mêmes données d'entrée, elle renvoie toujours le même résultat et ne provoque aucun effet de bord.

Règles d'une fonction pure :

  1. Déterminisme - même entrée = même sortie, toujours
  2. Aucun effet de bord - elle ne modifie rien en dehors de sa portée

La première fonction ci-dessous calcule le niveau de danger uniquement à partir de ses trois arguments. La seconde fait quelque chose qui a l'air innocent : au passage, elle incrémente un compteur déclaré en dehors d'elle.

1// FONCTION PURE - toujours le même résultat
2function calculateDangerLevel(weight, speed, aggression) {
3  return (weight * 0.3) + (speed * 0.3) + (aggression * 0.4);
4}
5
6// La même fonction appelée 1000 fois avec les mêmes arguments
7// renverra toujours le même résultat
8calculateDangerLevel(8000, 40, 95); // toujours 2450
9calculateDangerLevel(8000, 40, 95); // toujours 2450
10
11// FONCTION IMPURE - modifie des données externes
12let totalDinosaurs = 0;
13function addDinosaur(name) {
14  totalDinosaurs++; // effet de bord - modifie une variable externe !
15  return { name, id: totalDinosaurs };
16}
17// Chaque appel donne un résultat différent, même avec le même argument
18addDinosaur('Rex'); // { name: 'Rex', id: 1 }
19addDinosaur('Rex'); // { name: 'Rex', id: 2 } - résultat différent !

L'appel calculateDangerLevel(8000, 40, 95) donne 2400 + 12 + 38, soit 2450 - aujourd'hui, demain et à la millième répétition. Avec addDinosaur, c'est une autre histoire : le même argument 'Rex' donne deux résultats différents, car la fonction dépend de la variable totalDinosaurs, qu'elle modifie elle-même. C'est le mécanisme du bug du rapport du matin.

Un effet de bord, c'est toute modification du monde extérieur à la fonction : la modification d'une variable externe ou d'un objet reçu en argument, une entrée dans la console, une requête vers un serveur, un changement du contenu de la page. Le déterminisme, lui, est brisé par toute source de variabilité, par exemple un tirage au sort :

1// Impure : un effet de bord sous la forme d'une entrée dans la console
2function logSample(id) {
3  console.log('Échantillon :', id);
4  return id;
5}
6
7// Impure : le résultat dépend du hasard, pas des arguments
8function randomZone() {
9  return Math.random() > 0.5 ? 'A' : 'B';
10}

Aucune des deux ne touche aux données du parc, et pourtant toutes deux sont impures : logSample laisse une trace dans la console, et randomZone, sans aucun argument, renvoie tantôt 'A', tantôt 'B'. Toute fonction qui lit l'heure avec Date.now() est impure de la même façon.

Immutabilité (Immutability)

L'immutabilité est le principe selon lequel des données, une fois créées, ne sont jamais modifiées. Au lieu de modifier un objet existant, on crée une nouvelle copie qui contient les changements. C'est comme créer une nouvelle séquence d'ADN au lieu de modifier une séquence existante - plus sûr et plus prévisible.

Compare deux façons de noter que Rex a été blessé :

1// MUTATION - dangereuse (modification de l'objet original)
2const dinosaur = { name: 'Rex', health: 100 };
3dinosaur.health = 80; // L'objet original a été modifié !
4
5// IMMUTABILITÉ - sûre (nouvel objet)
6const dinosaurSafe = { name: 'Rex', health: 100 };
7const injuredDinosaur = { ...dinosaurSafe, health: 80 };
8// dinosaurSafe a toujours health: 100 (inchangé)
9// injuredDinosaur a health: 80 (nouvel objet)

L'opérateur spread (...) copie tous les champs dans un nouvel objet, et health: 80, écrit après lui, écrase un champ de la copie. Remarque aussi que const n'a pas protégé l'objet dinosaur contre la modification : il bloque la réaffectation de la variable, pas le contenu de l'objet. Nous reviendrons sur cette différence dans la leçon sur les motifs de données immuables.

Les fonctions comme valeurs de première classe

En JavaScript, les fonctions sont des valeurs de première classe (first-class citizens) : tu peux les affecter à des variables, les passer en arguments et les renvoyer depuis d'autres fonctions.

D'abord les deux premières possibilités : on enregistre la fonction dans une constante, puis on la passe à filter, comme on passe un échantillon d'un poste de travail à l'autre :

1// Fonction affectée à une variable
2const classify = (dino) => dino.diet === 'carnivore' ? 'dangerous' : 'safe';
3
4// Fonction passée en argument
5const dinosaurs = [
6  { name: 'Rex', diet: 'carnivore' },
7  { name: 'Brachio', diet: 'herbivore' },
8];
9const dangerous = dinosaurs.filter((dino) => classify(dino) === 'dangerous');

L'affectation de classify ne lance rien : c'est une recette que filter appellera pour chaque dinosaure. Dans le tableau dangerous, il ne reste que Rex.

La troisième possibilité est la plus intéressante, car une fonction peut construire et renvoyer une nouvelle fonction :

1// Fonction qui renvoie une fonction
2function createValidator(minWeight) {
3  return (dino) => dino.weight >= minWeight;
4}
5const isHeavyDino = createValidator(5000);
6isHeavyDino({ weight: 8000 }); // true
7isHeavyDino({ weight: 200 });  // false

createValidator(5000) produit un validateur prêt à l'emploi, qui se souvient du seuil minWeight, alors que l'appel de la fabrique est terminé depuis longtemps. Les fonctions qui acceptent ou renvoient d'autres fonctions s'appellent des fonctions d'ordre supérieur, et le mécanisme qui mémorise les variables est une fermeture (closure) - nous développerons ces deux sujets dans les prochaines leçons.

Pourquoi ça vaut le coup ?

Une fonction pure se teste sans préparer le monde autour d'elle : tu fournis les arguments et tu compares le résultat - tu te souviens de Jest, vu dans la location précédente ? Tu peux aussi mémoriser son résultat en toute sécurité, ou l'enchaîner avec d'autres fonctions. Tu n'écriras pourtant jamais de programme totalement dépourvu d'effets de bord, car il faut bien que quelqu'un affiche le rapport. C'est pourquoi je te recommande la règle du Dr Rex : garde les effets aux bords du programme (le chargement des données au début, console.log et l'enregistrement à la fin), et écris la logique du milieu sous forme de fonctions pures.

Dans l'éditeur ci-dessous, change les arguments et vérifie quelles fonctions se comportent comme une expérience contrôlée.

La programmation fonctionnelle se résume à une seule promesse : les mêmes données en entrée donnent toujours le même résultat, aucun échantillon n'est modifié en douce, et tu peux transmettre des fonctions aussi librement que n'importe quelle autre valeur du laboratoire.

Code de cette leçon : index.js
1// Programmation fonctionnelle - Introduction
2console.log("=== Jurassic Park - Laboratoire des Fonctions Pures ===\n");
3
4// EXEMPLE 1 : Fonction pure vs impure
5
6// FONCTION PURE - toujours le même résultat, aucun effet de bord
7function calculateDangerLevel(weight, speed, aggression) {
8  return (weight * 0.3) + (speed * 0.3) + (aggression * 0.4);
9}
10
11console.log("--- Fonctions pures ---");
12console.log("T-Rex danger:", calculateDangerLevel(8000, 40, 95));
13console.log("T-Rex danger:", calculateDangerLevel(8000, 40, 95));
14console.log("Resultats identiques ! Une fonction pure est deterministe.\n");
15
16// FONCTION IMPURE - effets de bord
17let totalRegistered = 0;
18function registerDinosaurImpure(name) {
19  totalRegistered++; // Effet de bord !
20  return { name, id: totalRegistered };
21}
22
23console.log("--- Exemple impur ---");
24console.log("Rex:", registerDinosaurImpure("Rex"));
25console.log("Rex:", registerDinosaurImpure("Rex"));
26console.log("Resultats differents ! La fonction depend d'un etat externe.\n");
27
28// EXEMPLE 2 : Immutabilite
29console.log("--- Immutabilite ---");
30const dinosaur = { name: "Rex", health: 100 };
31const injuredDino = { ...dinosaur, health: 80 };
32
33console.log("Original:", dinosaur);
34console.log("Nouvel objet:", injuredDino);
35console.log("Original inchange:", dinosaur.health === 100);
36
37// EXEMPLE 3 : Les fonctions comme valeurs
38console.log("\n--- Les fonctions comme valeurs ---");
39const classify = (dino) => dino.diet === "carnivore" ? "dangerous" : "safe";
40
41const dinos = [
42  { name: "Rex", diet: "carnivore" },
43  { name: "Brachio", diet: "herbivore" },
44  { name: "Raptor", diet: "carnivore" },
45];
46
47const dangerous = dinos.filter((d) => classify(d) === "dangerous");
48console.log("Dangereux:", dangerous.map((d) => d.name));
49
50// EXEMPLE 4 : Fonction renvoyant une fonction
51function createWeightFilter(minWeight) {
52  return (dino) => dino.weight >= minWeight;
53}
54
55const isHeavy = createWeightFilter(5000);
56console.log("\nRex (8000kg) est lourd ?", isHeavy({ weight: 8000 }));
57console.log("Raptor (150kg) est lourd ?", isHeavy({ weight: 150 }));

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. 1. Laquelle des fonctions suivantes est une fonction pure ?

Articles utiles