JavaScript – Foncteurs

Si vous souhaitez un texte plus fouillé sur le sujet, mais d'un point de vue C++, voir ../Divers--cplusplus/foncteurs.html

Le langage JavaScript est un langage fonctionnel, mettant l'accent sur la fonction comme entité naturelle. Dans ce langage, écrire une fonction qui accepte une fonction en paramètre et retourne une fonction est une chose toute naturelle. C'est d'ailleurs une force de ce langage, en particulier sur les architectures multi-coeurs contemporaines.

Cela dit, une fonction, lorsque prise au sens mathématique du terme, est un peu comme un poisson rouge : elle ne se souvient pas de ce qu'elle a fait d'un appel à l'autre, et ne mémorise pas ses états en cours de route. Cette caractéristique des fonctions facilite le raisonnement : si à un instant et si à un instant tel que , alors il va de soi que . C'est une vertu, n'est-ce pas? Avec des objets, nous n'avons pas cette simplicité, puisque les états internes d'un objet peuvent impacter les fruits des calculs que cet objet effectue.

Cela dit, il arrive que l'on souhaite qu'une fonction « apprenne » des informations, idéalement une seule fois, à la « construction », pour les utiliser à loisir par la suite. Une fonction qui se comporte comme un objet, en quelque sorte : qui s'utilise comme une fonction (écriture à l'utilisation : ) mais qui ait des états susceptibles d'influencer son exécution. Un tel hybride entre fonction et objet est ce que nous nommons, en programmation, un foncteur.

Exemple concret

Imaginons que nous souhaitions écrire une fonction unaire (à un seul paramètre) estNégatif retournant vrai seulement si la valeur de son paramètre est sous zéro. Un tel prédicat est somme toute banal à exprimer :

 function estNégatif(x) {
   return x < 0;
}

Une telle fonction peut être utile :

 function doublerSi(vals,pred) {
   for(var i = 0; i != vals.length; ++i) {
      if (pred(val[i])) {
         val[i] *= 2;
      }
   }
}
var vals = [ 1, -2, 3, -4, 5 ];
doublerSi(vals,estNégatif);
// ici, vals vaut [ 1, -4, 3, -8, 5 ]

Imaginons maintenant que nous souhaitions écrire une fonction estMoinsQueDix retournant vrai seulement si la valeur de son paramètre est sous dix. Un tel prédicat est aussi banal à exprimer :

 function estMoinsQueDix(x) {
   return x < 10;
}

Un problème saute aux yeux, déjà : cette approche n'est pas flexible, et n'est pas extensible. Si nous souhaitons exprimer l'idée qu'une fonction estMoinsQue pour un seuil choisi dynamiquement, nous ne voudrons probablement pas écrire une fonction pour chacune des valeurs de seuil possibles.

Nous ne voudrons probablement pas non plus transformer estMoinsQue en fonction binaire (à deux paramètres) puisque (a) l'un des deux paramètres (le seuil) ne changerait jamais, et (b) en changeant la signature de la fonction, nous ne pourrions plus utiliser la nouvelle dans les mêmes situations que l'originale (ici, une fonction comme doublerSi, qui attend comme second paramètre un prédicat unaire, ne pourrait pas utiliser une fonction qui aurait besoin de deux paramètres, la signature à l'appel ne correspondant pas à celle de la fonction à appeler).

La solution ici est un foncteur. En utilisant une fermeture (l'apprentissage d'une valeur dans le contexte d'une fonction qui sera éventuellement appelée), nous pouvons enrichir nos fonctions de capacités insoupçonnées. Comparez :

function estPlusPetitQue(x) { // <-- un foncteur
   return function(y) {
      return y < x; // fermeture sur x
   }
}
function doublerSi(vals,pred) {
   for(var i = 0; i != vals.length; ++i) {
      if (pred(val[i])) {
         val[i] *= 2;
      }
   }
}
var vals = [ 1, -2, 3, -4, 5 ];
doublerSi(vals,estPlusPetitQue(0)); // seuil appris dynamiquement!
// ici, vals vaut [ 1, -4, 3, -8, 5 ]

Allons-y d'un exemple plus riche :

// Pour les tests, j'utiliserai ce tableau choisi arbitrairement
function générerValeurs() {
   return [2,3,4,5,11,-7,8,999,4];
}
function testFoncteurs() {
   function plusPetitQue(x) {
      return function(y) {
         return y < x;
      };
   }
   function estNégatif() {
      return plusPetitQue(0);
   }
   function plusGrandQue(x) {
      return function(y) {
         return y > x;
      };
   }
   function égalÀ(x) {
      return function(y) {
         return y == x;
      };
   }
   function filtrerSi(vals,pred) {
      var résultat = new Array();
      var j = 0;
      for (var i = 0; i < vals.length; ++i) {
         if (!pred(vals[i])) {
            résultat[j++] = vals[i];
         }
      }
      return résultat;
   }
   function conserverSi(vals,pred) {
      return filtrerSi(vals, function(x) {
         return !pred(x);
      });
   }
   var vals = générerValeurs();
   alert("Valeurs: {" + vals + "}, après filtre des plus grands que 3: {" + filtrerSi(vals,plusGrandQue(3)) + "}");
   alert("Valeurs: {" + vals + "}, ne conservant que les plus grands que 3: {" + conserverSi(vals,plusGrandQue(3)) + "}");
   alert("Valeurs: {" + vals + "}, après filtre des plus petits que 3: {" + filtrerSi(vals,plusPetitQue(3)) + "}");
   alert("Valeurs: {" + vals + "}, ne conservant que les plus petits que 3: {" + conserverSi(vals,plusPetitQue(3)) + "}");
   alert("Valeurs: {" + vals + "}, après filtre des 3: {" + filtrerSi(vals,égalÀ(3)) + "}");
   alert("Valeurs: {" + vals + "}, ne conservant que les 3: {" + conserverSi(vals,égalÀ(3)) + "}");
   alert("Valeurs: {" + vals + "}, après filtre des négatifs: {" + filtrerSi(vals,estNégatif()) + "}");
   alert("Valeurs: {" + vals + "}, ne conservant que les négatifs: {" + conserverSi(vals,estNégatif()) + "}");
   //
   // ceci ne doit pas planter!
   //
   alert("Valeurs: {" + vals + "}, ne conservant que les 1: {" + conserverSi(vals,égalÀ(1)) + "}"); // retourne une séquence vide!
}

Pas mal, non?


Valid XHTML 1.0 Transitional

CSS Valide !