Skip to content

Chapitre 03 : Le bonheur par les fonctions pures

Retour à la pureté

La première chose à clarifier est le concept de fonction pure.

Une fonction pure est une fonction qui, pour les mêmes entrées, retourne toujours la même sortie et ne produit aucun effet secondaire observable.

Prenons slice et splice. Ces deux fonctions accomplissent la même tâche - selon des mécanismes totalement divergents, mais la même tâche néanmoins. slice est pure car elle garantit le même résultat pour les mêmes entrées. En revanche, splice modifie irréversiblement le tableau d'origine, introduisant ainsi un effet observable.

js
const xs = [1,2,3,4,5];

// pure
xs.slice(0,3); // [1,2,3]

xs.slice(0,3); // [1,2,3]

xs.slice(0,3); // [1,2,3]


// impure
xs.splice(0,3); // [1,2,3]

xs.splice(0,3); // [4,5]

xs.splice(0,3); // []

En programmation fonctionnelle, nous rejetons les fonctions encombrantes comme splice qui mutent les données. Nous privilégions des fonctions fiables produisant des résultats reproductibles, contrairement à splice qui génère des états chaotiques.

Examinons un autre exemple.

js
// impure
let minimum = 21;
const checkAge = age => age >= minimum;

// pure
const checkAge = (age) => {
  const minimum = 21;
  return age >= minimum;
};

Dans sa version impure, checkAge dépend de la variable mutable minimum pour déterminer son résultat. Cette dépendance à l'état système accroît la charge cognitive en introduisant un environnement externe.

Bien que cet exemple semble simple, cette dépendance à l'état est une source majeure de complexité systémique (http://curtclifton.net/papers/MoseleyMarks06a.pdf). Cette version de checkAge peut varier selon des facteurs externes, compromettant sa pureté et compliquant le raisonnement logique.

La version pure, quant à elle, est parfaitement autonome. En rendant minimum immuable, nous préservons la pureté. Cela nécessite de créer un objet figé.

js
const immutableState = Object.freeze({ minimum: 21 });

Les effets secondaires incluent...

Approfondissons ces « effets secondaires ». Un effet secondaire désigne toute interaction se produisant pendant le calcul en dehors du résultat final - modification d'état ou interaction avec l'environnement externe.

Les effets ne sont pas intrinsèquement mauvais, mais leur dimension « secondaire » pose problème. Comme l'eau stagnante génère des larves, les effets secondaires non contrôlés deviennent des nids à bugs.

Un effet secondaire est une altération de l'état système ou une interaction observable avec l'environnement externe durant un calcul.

Les effets secondaires incluent notamment :

  • Modifier le système de fichiers
  • Insérer un enregistrement en base de données
  • Effectuer un appel HTTP
  • Mutations
  • Afficher à l'écran / journaliser
  • Capturer une entrée utilisateur
  • Interroger le DOM
  • Accéder à l'état système

Cette liste est non exhaustive. Toute interaction externe invalide la pureté. La programmation fonctionnelle considère ces effets comme principaux responsables de comportements erratiques.

Il ne s'agit pas de les bannir, mais de les contrôler. Les foncteurs et monades (voir chapitres suivants) offriront ce contrôle. Pour l'instant, isolons les fonctions impures.

Les effets secondaires disqualifient la pureté. En effet : par définition, une fonction pure doit garantir la cohérence input/output, impossible avec des dépendances externes.

Explorons fondamentalement pourquoi la cohérence input/output est cruciale. Remontons aux mathématiques de collège.

Mathématiques niveau collège

Selon mathsisfun.com :

Une fonction est une relation spéciale entre valeurs : Chaque valeur d'entrée produit exactement une valeur de sortie.

Il s'agit simplement d'une relation entre deux éléments : l'entrée et la sortie. Bien que chaque entrée ait exactement une sortie, celle-ci n'est pas forcément unique. Ce schéma montre une fonction valide de x vers y :

ensembles de fonctions(https://www.mathsisfun.com/sets/function.html)

Ce schéma illustre une relation non fonctionnelle où l'entrée 5 pointe vers plusieurs sorties :

relation non fonctionnelle(https://www.mathsisfun.com/sets/function.html)

Une fonction peut s'écrire comme un ensemble de paires (entrée, sortie) : [(1,2), (3,6), (5,10)] (ici, l'entrée est doublée).

Table de correspondance :

Input Output
1 2
2 4
3 6

Ou sous forme graphique avec x en entrée et y en sortie :

function graph

Les détails d'implémentation deviennent superflus si l'entrée dicte la sortie. Les fonctions pures sont ces relations mathématiques en programmation.

js
const toLowerCase = {
  A: 'a',
  B: 'b',
  C: 'c',
  D: 'd',
  E: 'e',
  F: 'f',
};
toLowerCase['C']; // 'c'

const isPrime = {
  1: false,
  2: true,
  3: true,
  4: false,
  5: true,
  6: false,
};
isPrime[3]; // true

Pour les fonctions multivariées, on peut utiliser des tableaux ou le curryfiage (voir chapitre 04) pour respecter cette définition mathématique.

Révélation cruciale : les fonctions pures sont ces fonctions mathématiques, essence même de la programmation fonctionnelle. Leurs bénéfices méritent nos efforts pour préserver leur pureté.

Plaidoyer pour la pureté

Mémoïsation

Les fonctions pures sont mémoïsables par entrée via la technique de mémoïsation :

js
const squareNumber = memoize(x => x * x);

squareNumber(4); // 16

squareNumber(4); // 16, returns cache for input 4

squareNumber(5); // 25

squareNumber(5); // 25, returns cache for input 5

Implémentation simplifiée (des versions plus robustes existent) :

js
const memoize = (f) => {
  const cache = {};

  return (...args) => {
    const argStr = JSON.stringify(args);
    cache[argStr] = cache[argStr] || f(...args);
    return cache[argStr];
  };
};

Certaines fonctions impures peuvent devenir pures par évaluation différée :

js
const pureHttpCall = memoize((url, params) => () => $.getJSON(url, params));

Ici, nous ne déclenchons pas l'appel HTTP immédiatement, mais retournons une fonction pure décrivant l'action. Cette fonction reste pure car liée intrinsèquement à ses entrées.

Notre fonction memoize cache ici la fonction générée plutôt que le résultat HTTP.

Bien que limité actuellement, ce principe montre que toute fonction peut être mémoïsable, aussi destructrices soient-elles en apparence.

Portabilité / Auto-documentation

Les fonctions pures sont autonomes. Leurs dépendances sont explicites - rien n'est caché. Cette transparence facilite la maintenance.

js
// impure
const signUp = (attrs) => {
  const user = saveUser(attrs);
  welcomeUser(user);
};

// pure
const signUp = (Db, Email, attrs) => () => {
  const user = saveUser(Db, attrs);
  welcomeUser(Email, user);
};

La signature d'une fonction pure révèle toutes ses dépendances (Db, Email, attrs), contrairement aux méthodes opaques.

Nous verrons comment purifier ces fonctions sans délai d'exécution. L'approche pure est bien plus informative.

L'injection de dépendances (Db, client mail) rend l'application flexible. Changer de Db ne nécessite qu'un nouvel appel de fonction.

En JavaScript, cette portabilité permet la sérialisation de fonctions ou l'exécution dans des web workers.

Contrairement aux méthodes impératives liées à leur environnement, les fonctions pures sont ubiquitaires.

« Le problème des langages orientés objet est qu'ils transportent tout cet environnement implicite. Vous vouliez une banane mais vous avez eu un gorille tenant la banane... et toute la jungle » (Joe Armstrong, créateur d'Erlang).

Testabilité

Les fonctions pures simplifient les tests : pas besoin de simuler des environnements complexes. Entrée → Vérification de sortie.

Des outils comme Quickcheck (communauté fonctionnelle) génèrent automatiquement des entrées tests, validant les propriétés des sorties.

Raisonnement formel

L'avantage majeur est la transparence référentielle : remplacer un appel par sa valeur sans altérer le programme.

Les fonctions pures préservent cette propriété car leurs sorties dépendent uniquement des entrées.

js
const { Map } = require('immutable');

// Aliases: p = player, a = attacker, t = target
const jobe = Map({ name: 'Jobe', hp: 20, team: 'red' });
const michael = Map({ name: 'Michael', hp: 20, team: 'green' });
const decrementHP = p => p.set('hp', p.get('hp') - 1);
const isSameTeam = (p1, p2) => p1.get('team') === p2.get('team');
const punch = (a, t) => (isSameTeam(a, t) ? t : decrementHP(t));

punch(jobe, michael); // Map({name:'Michael', hp:19, team: 'green'})

Avec decrementHP, isSameTeam et punch pures, le raisonnement équationnel permet des substitutions « égal à égal » pour simplifier le code.

Exemple d'inline de isSameTeam :

js
const punch = (a, t) => (a.get('team') === t.get('team') ? t : decrementHP(t));

Données immuables ⇒ substitution directe des valeurs :

js
const punch = (a, t) => ('red' === 'green' ? t : decrementHP(t));

Élimination des branches conditionnelles inutiles :

js
const punch = (a, t) => decrementHP(t);

Après inline, punch devient un simple décrément de hp.

js
const punch = (a, t) => t.set('hp', t.get('hp') - 1);

Ce raisonnement facilite le refactoring. Utilisé dans notre programme 'mouette', il exploite les propriétés mathématiques des opérations.

Parallélisation

Coup de grâce : les fonctions pures peuvent s'exécuter en parallèle car sans mémoire partagée ni condition de concurrence.

Possible avec les threads Node.js ou les Web Workers, bien que la complexité des fonctions impures freine cette pratique.

En résumé

Les fonctions pures constituent la pierre angulaire de la programmation fonctionnelle. Désormais, nous viserons à écrire des fonctions pures, en isolant les impuretés.

Ce style nécessite des outils complémentaires (curryfiage) pour gérer la circulation des données sans état ni effets.

Chapitre 04 : Curryfication