JavaScript – penser « fonctionnel »

Bien que le langage JavaScript ne soit pas particulièrement complexe d'approche, du moins pour qui a une certaine habitude avec la syntaxe de langages tels que C, C++, Java ou C#, il arrive que le mode de pensée propre à la programmation fonctionnelle fonctionnelle, qui y est mis en valeur, surprenne les programmeuses et les programmeurs plus habitué(e)s à écrire des programmes impératifs ou orientés objets.

Ce petit texte a pour but de vous introduire à ce mode de pensée particulier (et plutôt fécond) que l'on nomme la programmation fonctionnelle. Nous procéderons à partir d'un programme impératif simple (très simple!), que nous ferons glisser vers une écriture plus proche des usages. J'espère que le résultat vous semblera élégant.

Énoncé du problème à résoudre

Pour les besoins de la cause, nous utiliserons un exemple simple, soit :

Solution impérative

Une solution impérative simple serait celle-ci :

// Pour les tests, j'utiliserai ce tableau choisi arbitrairement
function générerValeurs() {
   return [2,3,4,5,11];
}
//
// Les tests sont tous placés dans testImpératif(), pour éviter la pollution.
// Oui, ce sont des fonctions dans une fonction... Et c'est correct comme ça :)
//
function testImpératif() {
   function éleverAuCarré(tab) {
      var résultat = new Array(tab.length);
      for (var i = 0; i != tab.length; ++i) {
         résultat[i] = tab[i] * tab[i];
      }
      return résultat;
   }
   function négationArithmétique(tab) {
      var résultat = new Array(tab.length);
      for (var i = 0; i != tab.length; ++i) {
         résultat[i] = -tab[i];
      }
      return résultat;
   }
   function combinaison(tab) {
      var carrés = éleverAuCarré(tab);
      var négCarrés = négationArithmétique(carrés);
      return négCarrés;
   }
   var vals = générerValeurs();
   alert(combinaison(vals));
}

Portez attention et vous remarquerez une certaine dose de répétition, de redondance dans le code présenté ici. Ça fonctionne, mais sur le plan de la modularité et de la capacité de réutiliser des morceaux, on repassera.

Vers une solution fonctionnelle – premiers pas

Commençons par identifier les opérations de bas niveau, celles qui s'appliquent à un seul élément de chaque tableau : élever au carré, faire une négation arithmétique. Faisons-en de petites fonctions simples, nommées carré() et négation(). Avec ces fonctions, le code devient :

// Pour les tests, j'utiliserai ce tableau choisi arbitrairement
function générerValeurs() {
   return [2,3,4,5,11];
}
function testImpérativoFonctionnel() {
   function carré(x) {
      return x * x;
   }
   function négation(x) {
      return -x;
   }
   function éleverAuCarré(tab) {
      var résultat = new Array(tab.length);
      for (var i = 0; i != tab.length; ++i) {
         résultat[i] = carré(tab[i]);
      }
      return résultat;
   }
   function négationArithmétique(tab) {
      var résultat = new Array(tab.length);
      for (var i = 0; i != tab.length; ++i) {
         résultat[i] = négation(tab[i]);
      }
      return résultat;
   }
   function combinaison(tab) {
      var carrés = éleverAuCarré(tab);
      var négCarrés = négationArithmétique(carrés);
      return négCarrés;
   }
   var vals = générerValeurs();
   alert(combinaison(vals));
}

Bien que cette transformation semble banale, elle met en relief que éleverAuCarré() et négationArithmétique() sont en fait identiques à une fonction près. Ce que ces fonctions font est :

Dans le langage de tous les jours, une telle fonction est ce qu'on nomme un map, ou un transform. Je vais utiliser le terme transformer() ici puisqu'il faut faire un choix.

Vers une solution fonctionnelle – raffinement

 En généralisant l'application de fonction aux éléments d'une séquence, à l'aide de notre nouvelle fonction transformer(), nous obtenons ceci :

// Pour les tests, j'utiliserai ce tableau choisi arbitrairement
function générerValeurs() {
   return [2,3,4,5,11];
}
function testPlutôtFonctionnel() {
   function carré(x) {
      return x * x;
   }
   function négation(x) {
      return -x;
   }
   function transformer(tab,f) {
      var résultat = new Array(tab.length);
      for (var i = 0; i != tab.length; ++i) {
         résultat[i] = f(tab[i]);
      }
      return résultat;
   }
   function éleverAuCarré(tab) {
      return transformer(tab,carré);
   }
   function négationArithmétique(tab) {
      return transformer(tab,négation);
   }
   function combinaison(tab) {
      var carrés = éleverAuCarré(tab);
      var négCarrés = négationArithmétique(carrés);
      return négCarrés;
   }
   var vals = générerValeurs();
   alert(combinaison(vals));
}

Enfin, amenons la démarche plus loin et constatons que la combinaison des deux opérations est en fait une transformation appliquant la composition de deux fonctions, f et g ou, dans  notre cas, négation et carré, à chaque élément du tableau en entrée. À partir de ce constat, nous pouvons combiner les opérations en une seule fonction, ce qui représente une importante optimisation :

Solution fonctionnelle

Voici où tout cela nous mène :

// Pour les tests, j'utiliserai ce tableau choisi arbitrairement
function générerValeurs() {
   return [2,3,4,5,11];
}
function testFonctionnel() {
   function carré(x) {
      return x * x;
   }
   function négation(x) {
      return -x;
   }
   function composer(f,g) {
      return function(x) {
         return f(g(x));
      };
   }
   function transformer(tab,f) {
      var résultat = new Array(tab.length);
      for (var i = 0; i != tab.length; ++i) {
         résultat[i] = f(tab[i]);
      }
      return résultat;
   }
   function éleverAuCarré(tab) {
      return transformer(tab,carré);
   }
   function négationArithmétique(tab) {
      return transformer(tab,négation);
   }
   function combinaison(tab) {
      return transformer(tab, composer(négation,carré));
   }
   alert(combinaison(générerValeurs()));
}

Pas si mal, n'est-ce pas? Outre transformer(), qui pourrait être plus concise si on faisait un effort, aucune de nos fonctions n'a plus d'une instruction. Nous générons moins de tableaux temporaires qu'initialement, nous parcourons moins souvent le tableau, donc le code résultant sera à la fois plus rapide et moins gourmand en termes de mémoire. De plus, étant plus simple et plus succinct, il est moins susceptible de cacher des bogues, donc nous sommes moins susceptibles de devoir le déboguer!

Comparatif

À titre comparatif, examinons les implémentations initiale et finale de notre solution au problème posé à l'origine.

Avant (impératif) Après (fonctionnel)
function éleverAuCarré(tab) {
   var résultat = new Array(tab.length);
   for (var i = 0; i != tab.length; ++i) {
      résultat[i] = tab[i] * tab[i];
   }
   return résultat;
}
function négationArithmétique(tab) {
   var résultat = new Array(tab.length);
   for (var i = 0; i != tab.length; ++i) {
      résultat[i] = -tab[i];
   }
   return résultat;
}
function combinaison(tab) {
   var carrés = éleverAuCarré(tab);
   var négCarrés = négationArithmétique(carrés);
   return négCarrés;
}
var vals = générerValeurs();
alert(combinaison(vals));
function carré(x) {
   return x * x;
}
function négation(x) {
   return -x;
}
function composer(f,g) {
   return function(x) {
      return f(g(x));
   };
}
function transformer(tab,f) {
   var résultat = new Array(tab.length);
   for (var i = 0; i != tab.length; ++i) {
      résultat[i] = f(tab[i]);
   }
   return résultat;
}
function éleverAuCarré(tab) {
   return transformer(tab,carré);
}
function négationArithmétique(tab) {
   return transformer(tab,négation);
}
function combinaison(tab) {
   return transformer(tab, composer(négation,carré));
}
alert(combinaison(générerValeurs()));

Nous avons ajouté des fonctions dans la solution fonctionnelle, mais certaines (composer, transformer) sont générales et peuvent être placées dans des bibliothèques, alors qu'initialement, le code tout entier était associé au domaine du problème à résoudre. Si nous évacuons le code de bibliothèque, un comparatif plus légitime transparaît :

Avant (impératif) Après (fonctionnel)
function éleverAuCarré(tab) {
   var résultat = new Array(tab.length);
   for (var i = 0; i != tab.length; ++i) {
      résultat[i] = tab[i] * tab[i];
   }
   return résultat;
}
function négationArithmétique(tab) {
   var résultat = new Array(tab.length);
   for (var i = 0; i != tab.length; ++i) {
      résultat[i] = -tab[i];
   }
   return résultat;
}
function combinaison(tab) {
   var carrés = éleverAuCarré(tab);
   var négCarrés = négationArithmétique(carrés);
   return négCarrés;
}
var vals = générerValeurs();
alert(combinaison(vals));
function carré(x) {
   return x * x;
}
function négation(x) {
   return -x;
}
function éleverAuCarré(tab) {
   return transformer(tab,carré);
}
function négationArithmétique(tab) {
   return transformer(tab,négation);
}
function combinaison(tab) {
   return transformer(tab, composer(négation,carré));
}
alert(combinaison(générerValeurs()));

Concrètement, la solution fonctionnelle va à l'essentiel. Il n'y reste que ce dont nous avons vraiment besoin, et encore, négationArithmétique et éleverAuCarré pourraient être remplacées par leur implémentation, qui est dans chaque cas claire et explicite.

En espérant que tout ceci vous inspire!


Valid XHTML 1.0 Transitional

CSS Valide !