Chapitre 04 : Curryfication
« Impossible de vivre si 'vivre' est sans toi » (référence à la chanson You Can't Live If Livin' Is Without You)
Mon père m'avait expliqué que certains objets semblent superflus... jusqu'à ce qu'on les possède. Le micro-ondes en est un exemple. Les smartphones aussi. Les plus âgés se souviennent d'une vie épanouie sans internet. Pour moi, la curryfication fait partie de cette liste.
Le concept est simple : invoquer une fonction avec moins d'arguments qu'attendu. Elle retourne alors une nouvelle fonction prenant les arguments restants.
Vous pouvez choisir de fournir tous les arguments d'un coup ou progressivement.
const add = x => y => x + y;
const increment = add(1);
const addTen = add(10);
increment(2); // 3
addTen(2); // 12Nous créons ici une fonction add prenant un argument et retournant une fonction. Par mécanisme de closure, cette dernière mémorise le premier argument. L'appel avec tous les arguments simultanément reste fastidieux - c'est pourquoi nous utilisons une fonction helper curry pour simplifier la définition et l'appel.
Définissons quelques fonctions curryfiées pour l'illustration. Désormais, nous utiliserons notre fonction curry définie dans l'Annexe A - Fonctions essentielles.
const match = curry((what, s) => s.match(what));
const replace = curry((what, replacement, s) => s.replace(what, replacement));
const filter = curry((f, xs) => xs.filter(f));
const map = curry((f, xs) => xs.map(f));Le schéma adopté est crucial : nous positionnons stratégiquement les données manipulées (String, Array) comme dernier argument. La raison s'éclaircira à l'usage.
(La syntaxe /r/g est une expression régulière signifiant toutes les occurrences de la lettre 'r'. Consultez la documentation MDN pour en savoir plus.)
match(/r/g, 'hello world'); // [ 'r' ]
const hasLetterR = match(/r/g); // x => x.match(/r/g)
hasLetterR('hello world'); // [ 'r' ]
hasLetterR('just j and s and t etc'); // null
filter(hasLetterR, ['rock and roll', 'smooth jazz']); // ['rock and roll']
const removeStringsWithoutRs = filter(hasLetterR); // xs => xs.filter(x => x.match(/r/g))
removeStringsWithoutRs(['rock and roll', 'smooth jazz', 'drum circle']); // ['rock and roll', 'drum circle']
const noVowels = replace(/[aeiou]/ig); // (r,x) => x.replace(/[aeiou]/ig, r)
const censored = noVowels('*'); // x => x.replace(/[aeiou]/ig, '*')
censored('Chocolate Rain'); // 'Ch*c*l*t* R**n'Cette démonstration illustre la capacité à « pré-charger » une fonction avec un ou deux arguments pour obtenir une nouvelle fonction mémorisant ces paramètres.
Je vous invite à cloner le dépôt Mostly Adequate (git clone https://github.com/MostlyAdequate/mostly-adequate-guide.git), copier le code ci-dessus et l'expérimenter dans le REPL. La fonction curry et les éléments des annexes sont disponibles dans le module support/index.js.
Une version publiée sur npm est également accessible :
npm install @mostly-adequate/supportPlus qu'un jeu de mots / Sauce spéciale
La curryfication offre de multiples applications. Elle permet de générer des fonctions spécialisées comme hasLetterR, removeStringsWithoutRs et censored par simple application partielle.
Elle permet aussi d'adapter une fonction opérant sur des éléments individuels pour qu'elle traite des tableaux en l'encapsulant avec map :
const getChildren = x => x.childNodes;
const allTheChildren = map(getChildren);L'application partielle (fournir moins d'arguments que requis) réduit la redondance. Comparez notre fonction allTheChildren avec l'implémentation non-curryfiée de lodash (l'ordre des arguments diffère) :
const allTheChildren = elements => map(elements, getChildren);Nous évitons généralement de définir des fonctions spécifiques aux tableaux car map(getChildren) suffit. Même principe pour sort, filter et autres fonctions d'ordre supérieur (qui acceptent ou retournent des fonctions).
Les fonctions pures convertissent 1 entrée en 1 sortie. La curryfication incarne ce principe : chaque argument produit une nouvelle fonction attendant les paramètres restants. Ce mécanisme, cher lecteur, respecte rigoureusement 1 entrée → 1 sortie.
Même si la sortie est une fonction, la pureté est préservée. Autoriser plusieurs arguments simultanés n'est qu'une simplification syntaxique pour éviter les () superflus.
En résumé
La curryfication est un outil indispensable à avoir dans sa boîte à outils, permettant d'alléger considérablement le code fonctionnel.
Elle génère dynamiquement des fonctions utiles par application partielle, tout en préservant la définition mathématique des fonctions malgré l'arité multiple.
Découvrons maintenant un autre outil essentiel : composer.
Chapitre 05 : Programmation par composition
Exercices
Note sur les exercices
Dans cet ouvrage, vous rencontrerez des sections d'exercices. Ils peuvent être réalisés directement dans le navigateur via gitbook (recommandé).
Tous les exercices bénéficient de fonctions helper globales définies dans l'Annexe A, l'Annexe B et l'Annexe C. Certains exercices fournissent même des fonctions dédiées !
Astuce : validez votre solution par
Ctrl + Entréedans l'éditeur intégré.
Exécuter les exercices localement (optionnel)
Pour utiliser votre propre éditeur :
- clonez le dépôt (
git clone git@github.com:MostlyAdequate/mostly-adequate-guide.git) - accédez au dossier exercices (
cd mostly-adequate-guide/exercises) - utilisez Node.js v10.22.1 (ex:
nvm install). Voir le README - installez les dépendances avec npm (
npm install) - modifiez les fichiers exercise_* dans les dossiers de chapitre
- lancez la correction (ex:
npm run ch04)
Les tests unitaires fourniront des indications en cas d'erreur. Les solutions sont dans les fichiers *solution_**.
Pratiquons !
Pratiquons !
Refactoriser pour supprimer tous les arguments via application partielle. const words = str => split(' ', str);
Pratiquons !
Refactoriser pour éliminer les arguments via application partielle des fonctions. const filterQs = xs => filter(x => match(/q/i, x), xs);
Considérons la fonction suivante :
const keepHighest = (x, y) => (x >= y ? x : y);Pratiquons !
Refactoriser max pour ne référencer aucun argument en utilisant keepHighest.
const max = xs => reduce((acc, x) => (x >= acc ? x : acc), -Infinity, xs);