Skip to content

Chapitre 02 : Fonctions de première classe

Un bref rappel

Lorsque nous disons que les fonctions sont « de première classe », cela signifie qu'elles peuvent être traitées comme n'importe quel autre type de données. Concrètement, nous pouvons les manipuler comme des entités standard : les stocker dans des tableaux, les passer en paramètres, les assigner à des variables, et bien plus encore.

Ceci relève du JavaScript basique (niveau 101), mais mérite d'être rappelé car une recherche rapide sur github révèle une méconnaissance généralisée de ce concept. Illustrons par un exemple fictif :

js
const hi = name => `Hi ${name}`;
const greeting = name => hi(name);

Ici, l'encapsulation de hi dans greeting est totalement redondante. Pourquoi ? Car les fonctions sont invoquables en JavaScript. Avec () ajouté, hi s'exécute et retourne une valeur. Sans cela, elle retourne simplement la fonction stockée dans la variable. Vérifions :

js
hi; // name => `Hi ${name}`
hi("jonas"); // "Hi jonas"

Puisque greeting ne fait qu'appeler hi avec le même argument, nous pourrions simplement écrire :

js
const greeting = hi;
greeting("times"); // "Hi times"

Autrement dit : hi est déjà une fonction attendant un argument. Pourquoi l'englober dans une autre fonction faisant exactement la même chose ? C'est aussi absurde qu'enfiler un manteau d'hiver en plein juillet pour régler le climatiseur et réclamer une glace.

Entourer une fonction d'une autre fonction simplement pour retarder son évaluation est d'une verbosité exaspérante et, en l'occurrence, une mauvaise pratique (nous verrons pourquoi sous peu, mais cela concerne la maintenance).

Maîtriser ce concept est crucial avant de continuer. Examinons d'autres exemples issus de bibliothèques npm.

js
// ignorant
const getServerStuff = callback => ajaxCall(json => callback(json));

// enlightened
const getServerStuff = ajaxCall;

Le code Ajax mondial regorge de ce schéma. Explication d'équivalence :

js
// this line
ajaxCall(json => callback(json));

// is the same as this line
ajaxCall(callback);

// so refactor getServerStuff
const getServerStuff = callback => ajaxCall(callback);

// ...which is equivalent to this
const getServerStuff = ajaxCall; // <-- look mum, no ()'s

Voilà comment procéder correctement. Répétons-le pour bien ancrer le raisonnement :

js
const BlogController = {
  index(posts) { return Views.index(posts); },
  show(post) { return Views.show(post); },
  create(attrs) { return Db.create(attrs); },
  update(post, attrs) { return Db.update(post, attrs); },
  destroy(post) { return Db.destroy(post); },
};

Ce contrôleur grotesque contient 99% de superflu. Réécriture possible :

js
const BlogController = {
  index: Views.index,
  show: Views.show,
  create: Db.create,
  update: Db.update,
  destroy: Db.destroy,
};

... ou suppression pure, car il ne fait que regrouper Views et Db inutilement.

Pourquoi privilégier les fonctions de première classe ?

Analysons les raisons. Comme illustré par getServerStuff et BlogController, ajouter des couches d'indirection inutiles génère du code redondant et nuit à la maintenabilité.

De plus, toute modification d'une fonction encapsulée nécessitera d'adapter son wrapper.

js
httpGet('/post/2', json => renderPost(json));

Si httpGet évolue pour gérer une erreur err, il faudra modifier « l'interface » associée.

js
// go back to every httpGet call in the application and explicitly pass err along.
httpGet('/post/2', (json, err) => renderPost(json, err));

Avec une approche en fonction de première classe, les modifications sont réduites :

js
// renderPost is called from within httpGet with however many arguments it wants
httpGet('/post/2', renderPost);

Outre la suppression de fonctions superflues, le nommage des paramètres pose problème. Les désignations inexactes - surtout sur du code legacy soumis à des évolutions - deviennent sources d'erreurs.

Multiplier les appellations pour un même concept génère de la confusion. Prenons ces deux fonctions identiques, dont l'une est bien plus générique :

js
// specific to our current blog
const validArticles = articles =>
  articles.filter(article => article !== null && article !== undefined),

// vastly more relevant for future projects
const compact = xs => xs.filter(x => x !== null && x !== undefined);

Un nom spécifique (ex: articles) nous lie artificiellement à un type de donnée, entraînant des réimplémentations inutiles.

Attention à this : si une fonction sous-jacente l'utilise et est appelée directement, des fuites d'abstraction peuvent survenir.

js
const fs = require('fs');

// scary
fs.readFile('freaky_friday.txt', Db.save);

// less so
fs.readFile('freaky_friday.txt', Db.save.bind(Db));

Db lié à son prototype peut accéder à du code parasite. J'évite this comme une couche sale. Inutile en programmation fonctionnelle, mais parfois requis pour interagir avec d'autres bibliothèques.

Certains invoquent l'optimisation pour justifier this. Si vous êtes adepte des micro-optimisations, refermez ce livre. Échangez-le contre un traité de niche si nécessaire.

Sur ce, passons à la suite.

Chapitre 03 : Bonheur fonctionnel avec les fonctions pures