Introduction à la curryfication en JavaScript

Implémentations et exemples pratiques

Hicham Benjelloun
Le JavaScript
8 min readJul 22, 2020

--

Photo by JJ Ying on Unsplash

Introduction

Nous souhaitons spécialiser une fonction JavaScript à plusieurs paramètres en la transformant d’abord en une composition de fonctions d’arités inférieures.

Dans un premier temps, considérons une fonction de la forme suivante :

Dans sa forme la plus simple, ce problème consiste à transformer cette fonction en une nouvelle fonction de la forme suivante :

Nous pouvons également généraliser ce processus pour obtenir la forme suivante :

Cette dernière transformation est aussi appelée curryfication (ou currying) en hommage au mathématicien américain Haskell Curry dont les travaux ont posé les bases de la programmation fonctionnelle.

Dans cet article, nous proposons d’étudier différents aspects de la curryfication en JavaScript. En particulier, nous verrons :

  • un exemple simple pour comprendre ce qu’est la curryfication en pratique.
  • plusieurs implémentations permettant de réaliser les transformations ci-dessus.
  • une variante générique et flexible consistant à transformer une fonction JavaScript en une composition de fonctions d’arités variables.
  • un exemple illustrant les limites des implémentations proposées au regard de la curryfication de fonctions d’arité variable.

Un exemple simple de curryfication

Nous souhaitons calculer le prix toutes taxes comprises (TTC) de produits appartenant à différentes catégories à l’aide d’une fonction TTC :

Réécrivons maintenant notre fonction TTC sous sa forme curryfiée :

Cela nous permet en particulier de définir des fonctions de calcul du prix TTC par catégorie :

Nous pouvons ainsi réécrire nos calculs de prix TTC sans répéter la valeur du taux utilisé :

Il s’agit là d’un exemple très simple permettant d’utiliser la curryfication pour pouvoir ensuite spécialiser une fonction en en fixant le premier paramètre.

Même si ce n’est pas le sujet de cet article, notez que nous pouvons également réécrire nos fonctions TTC_service et TTC_première_nécessité directement de la façon suivante :

Cette transformation revient à créer une application partielle. Nous dirons également que nous avons projeté la fonction TTC sur le reste des paramètres qui n'ont pas été fixés. L'avantage de cette technique et de pouvoir fixer certains paramètres et de retourner une nouvelle fonction d’arité inférieure.

Même si, comme l’exemple ci-dessus le montre, la projection de la fonction TTC sur le paramètre prix donne une fonction similaire à la fonction curryfiée, il ne faut pas confondre curryfication et application partielle :

  • la curryfication permet de transformer une fonction à plusieurs paramètres en une composition de fonctions unaires.
  • une application partielle permet de fixer un certain nombre de paramètres d’une fonction et retourne une fonction d’arité inférieure.

L’objet de cet article est d’implémenter une fonction curry générique permettant de réaliser un processus de curryfication de façon automatique, et ce, quel que soit le nombre de paramètres, fixé, de la fonction considérée.

Ce serait user de bien grands moyens dans le cas de cet exemple qui se veut simple, mais nous voudrions pouvoir obtenir, par exemple, la deuxième fonction TTC à partir de la première de la façon suivante :

Premières fonctions de curryfication

Curryfication simple

Considérons une fonction de la forme suivante :

Nous souhaitons la transformer en une nouvelle fonction de la forme suivante :

Une première solution

Voici une première solution possible :

Ici, la fonction curryOne prend en paramètre une fonction fn et retourne une nouvelle fonction prenant un paramètre first. Lorsque nous appelons la fonction unaire ainsi produite, nous obtenons une nouvelle fonction ayant mémorisé le premier argument passé au paramètre first, et appliquable au reste des paramètres de la fonction fn.

Une solution plus concise

Enfin, notez que la fonction curyOne peut se réécrire de la façon plus concise suivante :

En effet, la méthode Function.prototype.bind() renvoie ici une nouvelle fonction disposant d'un argument lié qui sera automatiquement placé avant les autres arguments qui lui seront passés, ce qui est très commode pour réaliser la transformation attendue.

Curryfication générale

Considérons maintenant la forme générale de la fonction curryfiée que nous souhaitons obtenir :

Avant de nous attaquer à l’implémentation, reprenons la solution du cas de la curryfication simple :

Et essayons ensuite d’aller un peu plus loin en appliquant à la première fonction retournée une nouvelle curryfication simple :

Pour pouvoir généraliser cette solution, nous devons d’abord observer que dans tous les cas, c’est la dernière fonction qui se charge d’appeler la fonction initiale fn sur l'ensemble des arguments. Par ailleurs, les arguments rencontrés lors du parcours des fonctions unaires successives s'empilent de gauche à droite dans l'appel final : il faut donc trouver un mécanisme pour que cette fonction puisse mémoriser dans le bon ordre tous les arguments passés successivement aux fonctions unaires.

Une première solution

Voici une première solution possible correspondant à l’observation précédente :

Nous construisons ici une fonction curryfiée curried à l'aide d'une récursion ascendante correspondant à l'accumulation dans acc des arguments passés successivement aux fonctions unaires :

  • Cas terminal : tous les arguments ont été consommés et la longueur de l’accumulateur d’arguments est égale à l’arité fn.length de la fonction.
  • Si tous les arguments n’ont pas encore été consommés, nous retournons alors une fonction unaire p => curried(...acc, p). En particulier, nous observons qu'à chaque appel de curried, la longueur de l'accumulateur est incrémentée de un et notre récursion se termine bien lorsque sa longueur atteint l'arité de la fonction fn.

Une solution plus concise

Enfin, notez que de la même façon que dans la section précédente, nous pouvons écrire une fonction plus concise en utilisant la méthode Function.prototype.bind() qui accumule naturellement les arguments à gauche.

Curryfication flexible

Il se peut que nous ayons besoin d’un peu plus de flexibilité lors de la curryfication d’une fonction.

Imaginons que nous souhaitions pouvoir spécialiser une fonction en spécifiant plusieurs arguments d’un coup au lieu de réaliser plusieurs appels fonctionnels :

Une première solution

Nous pouvons simplement reprendre la fonction de curryfication générale vue précédemment et remplacer les fonctions unaires par des fonctions d’arités variables :

Nous pouvons simplifier cette solution. En effet, nous utilisions une constante intermédiaire curried pour nous assurer que nous retournions initialement une fonction unaire lorsque nous retournions l'appel curried(). Maintenant, comme notre fonction curryfiée peut prendre n'importe quelle nombre d'arguments en paramètres, nous pouvons réécrire curry sans fonction intermédiaire :

En place de curried, nous écrirons simplement l'appel récursif principal avec curry(fn).

Une solution plus concise

L’usage de la méthode Function.prototype.bind() permet d'écrire la fonction précédente de façon plus concise, en accumulant de façon implicite les arguments dans la fonction retournée à chaque appel récursif :

Limites des implémentations proposées

Voici un exemple de fonction générique astucieuse nous permettant de calculer une somme de nombres de plusieurs façons :

Essayons maintenant de réutiliser la fonction curry générique écrite précédemment pour rendre plus lisible ce code :

Oups ! Vous vous en doutiez probablement, mais la ligne faisant appel à curry ne produit pas le bon résultat : cela est dû au fait que pour une fonction fn acceptant un nombre variable d'arguments, la valeur de fn.length qui définit le cas terminal de notre récursion vaut… 0 !

Ainsi, nous tombons directement dans le cas terminal de notre fonction récursive et notre tentative de curryfication revient à faire :

Cela n’est bien sûr pas ce que nous cherchons à faire mais ce problème nous permet d’insister sur le fait que notre fonction de curryfication s’applique seulement à une fonction ayant une arité fixe.

Dans le cas où vous auriez quand même besoin d’avoir des fonctions d’arités variables à curryfier, vous pouvez toujours modifier le code de la fonction curry pour prendre en compte un paramètre supplémentaire en plus de la fonction et qui indiquerait le nombre de paramètres attendus au total : il faut bien que notre fonction sache quand s'arrêter et retourner une valeur.

Cela étant dit, nous pouvons généraliser notre fonction curry dans certains cas. Par exemple, en généralisant la fonction sum vue au début de cette section, nous pouvons écrire :

Voilà donc une fonction de curryfication astucieuse de fonctions d’arités variables. Notez toutefois qu’il est nécessaire de préciser à JavaScript que nous souhaitons extraire la valeur ainsi obtenue étant donné que comme nous ne savons pas à l’avance où nous allons nous arrêter, la valeur retournée est toujours une fonction ! Ainsi, l’astuce consistant à utiliser la propriété valueOf nous permet d'extraire la valeur qui nous intéresse lorsque nous en avons besoin.

--

--