Les lambdas en Java...script

Je vais profiter de l’actualité Java 8 et Javascript pour aborder un sujet que je pousse à chaque fois que l’occasion se présente : la programmation fonctionnelle – PF en abrégé.

On parle beaucoup de ‘lambda’ en ce moment chez les javaistes et principalement comme d’un moyen d’alléger le code… mais c’est l’arbre qui cache la forêt !

Programmation fonctionnelle et lambdas js

Qui parle de lambda parle de ‘fonction’ en tant que concept autonome, entité manipulable, réutilisable, composable. Ce n’est pas qu’une astuce, ou un sucre syntaxique pour réduire le nombre de lignes de nos programmes.

Disposer de la ‘fonction’ comme d’un élément structurant de base pour nos programmes (en Java, langage orienté objet, c’est la classe qui joue ce rôle) nous ouvre un champ de possibilités très vaste ; Cela va beaucoup plus loin que le simple allègement du code, et on ne peut décemment pas l’ignorer lorsque l’on cherche l’élégance, la concision et l’expressivité dans les programmes.

Javascript, langage très souvent décrié et mal considéré, qui fait un come-back plutôt remarqué ces derniers temps avec le développement front-end, nous propose justement de structurer nos programmes autour des fonctions.

Alors, affutez vos fondamentaux en JS, oubliez tous vos réflexes de programmeur objet/impératif et chaussez vos escarpins de randonnée : revisitons le JS à la sauce fonctionnelle, en commençant dans ce premier article par débroussailler quelques fondamentaux.

 

Vous prendrez bien un peu de lambda calcul ?

Impossible donc d’y couper en ce moment avec la sortie de java 8 : Lambda par-ci, Lambda par-là…

Lambda => λ => ‘fonction anonyme’.

Ce terme fait référence au lambda-calcul (λ-calcul), un système formel qui fonde les concepts de fonction et d’application de fonction à des arguments. Tout un univers gravitant autour du concept de fonction comme centre des préoccupations (approche plutôt étrange pour des OOP’istes, adeptes de la terre plate, pour qui une ‘fonction’ est avant tout une méthode, encapsulant un comportement d’instance/de classe).

Un univers très mathématiques, certes, mais qui énonce des règles et axiomes très puissants et intéressants pour qui souhaite en tirer parti dans le développement informatique.

 

Un argument, ça va…

Un petit exemple de ‘déclaration de fonction’ en Lambda-calcul ?

λx.x

Comprendre : je décris une fonction anonyme qui lie un paramètre, x, à une formule qui une fois appliquée vaudra x (vous l’aurez peut-être reconnue, cette fonction : c’est la fonction identité).
Notez que le nom du paramètre importe peu (le lambda calcul parle d’alpha équivalence) :

λx.x ≡ λy.y ≡ λ@.@

Appliquez à cette expression une valeur revient à donner une valeur à x et substituer la valeur dans la formule :

[λx.x] avec x = 2 nous donne : [2]

Traduisons rapidement ceci en langage de programmation afin de retenir ceux qui pensent déjà à s’enfuir :

  // Lambda calcul
  λx.x

  // Javascript
  function (x) {
    return x;
  }

  // Java 8
  (x : int) -> x

  // Scala
  (x : Int) => x

Rien de bien compliqué ! ‘Appliquer’ des valeurs revient à ‘exécuter’ ces fonctions en leur passant une valeur en paramètre :

  // Javascript : ‘Application’ de la fonction avec le paramètre ‘2’
  (function (x) {
    return x;
  }) (2)
  // -> 2

 

Notez deux choses :

  1. les notations Java 8 et Scala sont plus proches du concept d’application (j’associe un paramètre nommé à une expression, et je remplacerai la valeur dans l’expression par substitution au runtime) que la version javascript, qui souligne plus la notion d »appel’ avec ‘retour’ (échange de messages).
  2. que le nombre d’arguments d’une fonction est aussi appelé arité. Dans notre exemple précédent : arité = 1.

Un autre petit exemple ?

  // Lambda calcul : fonction ‘élever au carré’
  λx.x*x

  // Javascript
  (function (x) {
    return x*x;
  }) (2)
  // -> 2*2 -> 4

 

Deux arguments : ça va mieux !

Voyons maintenant la syntaxe d’une fonction à deux arguments (arité = 2) :

  λx.λy.x+y

Je parie que vous vous attendiez à λxy.x+y ? (si ce n’est pas le cas, vous avez sûrement triché).

Dans le lambda-calcul, les deux possibilités sont en fait correctes et équivalentes (je ne m’amuserai pas à ressortir la démonstration…). Elles aboutissent au même résultat une fois appliquées. Mais elles ne correspondent pas tout à fait à la même chose si on les transpose en code :

  // Lambda calcul
  λxy.x+y

  // Javascript
  function (x,y) {
    return x + y;
  }

  // Lambda calcul
  λx.λy.x+y

  // Javascript
  function (x) {
    return function (y) {
      return x + y;
    }
  }

(Ressortez vite « Javascript, the definitive guide » si vous sentez votre JS vaciller)

Essayez d’appliquer x et y à nos deux variantes de ‘lambda expression’ : même résultat au final…
Sauf que cette application ne se fera pas de la même façon dans notre langage de programmation en fonction de la forme utilisée : soit x et y en même temps dans le premier cas, soit x puis y dans le deuxième.

  // Javascript

  (function (x,y) {
    return x + y;
  }) (2,5)
  // -> 7

  (function (x) {
    return function (y) {
      return x + y;
    }
  }) (2)(5)
  // -> 7

 

Voilà déjà un apport intéressant de la théorie ! La lambda calcul me dit que je peux considérer une fonction d’arité n comme autant de fonction d’arité 1, chacune étant une étape d’une application / exécution, plus complexe, partiellement résolue à chaque étape !

Quel intérêt me direz-vous ? J’étais moi aussi plutôt sceptique, au début. Mais nous verrons que dans certaines situations, cette propriété va nous être extrêmement utile pour arranger certaines tournures de style poussives en pirouettes fonctionnelles particulièrement élégantes.

Le terme officiel pour désigner ce passage (arité n) -> (n * arité 1) est ‘Curryfication‘ (ou ‘Schönfinkelisation’, mais c’est plus difficile à prononcer).

 

Mais où veut-il en venir ?

Ces exemples simples, en plus de glisser un peu de lambda calcul pour la culture, visent à amener la concentration du lecteur sur le concept même de fonction, de rappeler qu’une fonction peut retourner une autre fonction, et à présenter un premier outil fonctionnel de base, la curryfication, permettant de jouer sur la façon dont on va les invoquer (en une fois ou en plusieurs fois).

 

Here be dragons

La programmation fonctionnelle ne se résume cependant pas à cela (la fonction comme brique de base) : c’est aussi un certain nombre de règles, conventions et axiomes, hérités des mathématiques et du lambda calcul où il n’y a pas de place à l’improvisation, aux ‘hacks’ et au débuggage. Certains langages (lisp, haskell…) sont génétiquement fonctionnels, d’autres non.

C’est le cas des langages modernes mainstream (Javascript y compris), qui laissent beaucoup de liberté aux développeurs : prenez, voici des boucles for, des variables, des classes pour encapsuler un peu… Cette liberté a malheureusement comme effet secondaire de rendre ceux-ci (les programmes, pas les développeurs) souvent durs à comprendre, maintenir, débugger. Les plats de spaghettis ne sont pas rares ; et même avec la meilleure volonté du monde, un programme objet multi-couches multithreadé transbahutant des objets de droite et gauche fini toujours par atteindre un niveau de complexité trop élevé pour rester longtemps fréquentable. Les développeurs le fuient, les utilisateurs craignent ses foudres…

Il y a donc deux catégories de langages : les langages purement fonctionnels, rigoristes, élégants, et les autres…

Les principes de base de la PF

En Javascript (deuxième catégorie), il va donc falloir nous astreindre à appliquer autant que faire se peut les principes fonctionnels à force de discipline et de méthode, et ne pas nous laisser (du moins, pas trop) tenter par les facilités laxistes qui nous sont offertes.

Quelle discipline, quels principes mettre en œuvre et respecter?

  • N’utiliser que des fonctions ‘pures’ : sans effets de bords
  • Ne pas utiliser de variables, uniquement des constantes
  • Ecrire ses programmes à partir de fonctions et, si possible, uniquement de fonctions
  • Les fonctions peuvent prendre en paramètres et renvoyer d’autres fonctions
  • ‘Fonction’ est un type de donnée comme les autres (int, string…) et doit être utilisé comme tel.

Effets de bords

Le principe…

Par effet de bords, comprendre dommages collatéraux : on appelle une fonction en lui passant un paramètre, on récupère une valeur de retour… et le paramètre, ou quelque chose d’autre, à été modifié, ailleurs, dans le programme. Vous, développeur, devez examiner le contenu de la fonction pour savoir exactement quels impacts son appel pourra avoir. Encapsulation, quelqu’un ? Je passerais sous silence les fonctions de type getSomeValue() qui modifient des variables lorsqu’elles sont appelées – si possible sans rapport avec le SomeValue, pour bien faire.

Pour illustrer, un peu de Javascript pas très fonctionnel (ni utile, d’ailleurs) :

  var a = 1;
  var b = 41;
  function compute(){
    a += b;
  }
  compute();
  alert(‘final value is ‘ + a);

Défauts :

  • la fonction compute agit sur un élément extérieur => effet de bord observable, pas bien !
  • la fonction compute utilise une information de son environnement pour fonctionner (valeur de b) => une fonction ne devrait se baser que sur les arguments qui lui sont passés (sauf dans un cas particulier dont nous parlerons plus tard) !
  • la variable ‘a’ est modifiée après avoir été initialisée => pas bien (cf. utiliser uniquement des constantes) !

Version fonctionnelle, sans ces défauts :

function compute(a, b){
return a + b;
}
alert(‘final value is ‘ + compute(1, 41));

 

L’exemple est trivial (et assez artificiel) et peut sembler anodin (juste un ‘rewrite’ de mauvaises pratiques). Mais imaginez cet embryon d’algorithme se diluer dans une fonction plus vaste… les variables s’empiler… il est tard, vous devez résoudre ce petit bug pour pouvoir livrer… Vous savez bien de quoi je parle !

Les fameuses fonctions de 1200 lignes bourrées de if – else et de variables, modifiées au fil de l’eau, sont faites de ces tout petits raccourcis qu’on se permet de prendre et qui au final coûtent très (trop) cher.

Donc : pas d’effets de bords, la fonction ne travaille et n’agit que sur et dans son corps et n’en sort jamais, et renvoie une valeur résultante de son application.

Une discipline, certes, mais dont les avantages à la longue n’ont pas de prix.

En pratique

Techniquement donc, toute fonction se doit de ‘retourner’ quelque chose. Autrement, elle ne sert à rien, puisque les effets de bords sont interdits.

Bien évidemment, pour qu’un programme soit utile, certains effets de bords doivent pouvoir avoir lieu : sortie console, enregistrement en base de données… autant d’effets observables en dehors des fonctions dont on ne peut se passer complètement.

On tache donc d’isoler le plus possible dans nos programmes les fonctions ‘pures’ des fonctions ‘impures’ à effets de bords, en les isolants les unes des autres et en les composant intelligemment.

En Javascript, une convention et de retourner ‘undefined’ lorsqu’une fonction génère un effet de bord (return void 0). C’est ce que ‘vaut’ toute fonction n’effectuant pas de return ; console.info(‘hello’) par exemple vaut ‘undefined’. C’est l’équivalent du ‘void’ qu’on trouve dans d’autres langages.

Les variables, pour une stabilité des programmes… variable.

Une variable est un identifiant auquel est associé une valeur qui peut changer dans le temps. C’est l’une des bases de l’objet : état et mutation d’état. Les problèmes apparaissent lorsque l’état en question est modifié à plusieurs endroits différents dans un programme, parfois par plusieurs threads simultanés. Avec un certain nombre de précautions, et en se limitant à des scopes très localisés (au hasard, le corps d’une fonction), la mutation d’état n’est pas un problème. Mais propager un objet entre plusieurs fonctions, chacune étant autorisée a en altérer les informations, est une source potentielle de bugs, pouvant parfois être très difficiles à comprendre, reproduire et éliminer sans l’aide d’un débuggeur et d’un déroulement pas à pas fastidieux.

Aussi, en programmation fonctionnelle, on préfèrera éviter les mutations d’états et ne travailler qu’avec des constantes – soit en s’appuyant sur une mécanique du langage (les val en Scala), soit en s’interdisant de le faire explicitement, à défaut. L’ultime but étant de ne plus avoir de variable du tout et de ne jongler exclusivement qu’avec des fonctions et des combinaisons de fonctions… et nous verrons que l’on peut aller très loin dans cette direction, en mettant en oeuvre des concepts de PF aux noms exotiques (Combinateurs, Monades, Kleisli…) mais terriblement concrets et efficaces.

Des programmes uniquement composés de fonctions

Ecrire ses programmes avec uniquement des fonctions ? Mais où sont les boucles ?

Parlons en, des boucles. De quoi est constituée une boucle ? D’un accumulateur, d’une condition d’itération et d’un corps de l’itération.

  var cahier = [ ],
      pageInitiale = 0,
      pageCourante = 0;

  for(; pageCourante < cahier.length; page++){
    effectueCollageSur(cahier[pageCourante]);
  }

 

Pas fonctionnel. Améliorons ça : commençons par supprimer la boucle et isoler le corps de celle-ci dans une fonction.

  var cahier = [ ],
      pageInitiale = 0,
      pageCourante = 0;

  function itererSurCahier(cahier) {

    effectueCollageSur(cahier[pageCourante]);

  }

 

Problème : notre code ne boucle pas, puisque l’on a bien évidemment supprimé le for.
Introduisons une fonction intermédiaire iterationSuivante passant la page courante en paramètre :

  var cahier = [],
      pageInitiale = 0,
      pageCourante = 0;

  function itererSurCahier(cahier) {

    function iterationSuivante(page){
      effectueCollageSur(cahier[page]);
    }
  }

 

Appellons maintenant naïvement iterationSuivante avec pageInitiale, juste pour voir :

  var cahier = [],
      pageInitiale = 0,
      pageCourante = 0;

  function itererSurCahier(cahier) {

    iterationSuivante(pageInitiale);

    function iterationSuivante(page){
      effectueCollageSur(cahier[page]);
    }

  }

L’appel initial d’itererSurCahier se fait en allant ‘chercher’ la valeur de pageInitiale en dehors de la fonction : c’est bien sûr à proscrire, nous allons donc passer cette valeur en paramètre d’itererSurCahier :

  var cahier = [],
      pageCourante = 0;

  function itererSurCahier(cahier, pageInitiale) {

    iterationSuivante(pageInitiale);

    function iterationSuivante(page){
      effectueCollageSur(cahier[page]);
    }

  }

Il ne manque alors plus qu’à réintroduire l’incrémentation et la condition d’itération, puis à itérer :

  var cahier = [];

  itererSurCahier(cahier, 0);

  function itererSurCahier(cahier, pageInitiale) {

    iterationSuivante(pageInitiale);

    function iterationSuivante(page){
      effectueCollageSur(cahier[page]);

      if( (page+1) < cahier.length){
        iterationSuivante(page +1);
      }
    }
  }

Et voilà ! Plus de boucle au sens ‘programmation procédurale’ de base. Plus de ‘variable’ de boucle qui change d’état à chaque itération. La boucle ‘for’ ne serait donc qu’une sorte de fonction capable de s’auto appeler automatiquement en se passant les paramètres adéquats ?

Vous l’aurez sûrement reconnu : il s’agit de la récursivité. Cette pratique, courante en PF, peut dérouter un œil néophyte habitué aux structures de contrôle de boucles impératives, mais n’a absolument rien de compliqué à mettre en œuvre une fois le concept assimilé. Attention à la condition d’itération pour éviter les boucles infinies !

Pour répondre aux éventuelles interrogations :

  • les appels récursifs présentent un désavantage que nous aborderons dans un futur article (débordement de pile)
  • oui, cette forme est plus verbeuse que la boucle for ; mais nous découvrirons, là aussi dans un futur article, un outil fonctionnel nous permettant de réduire ce code à… une ligne.
  • oui, j’ai modifié l’objet original (cahier) dans ma fonction, créant un effet de bord, indésirable en PF ; nous verrons comment résoudre ce problème…

First Class Citizen, Higher order

Un langage de programmation fonctionnel doit me permettre de faire ceci :

  // Je stocke ma fonction dans une variable => first class citizen
  var f = function(){
    …
  };

  // Higher Order => ordre supérieur => reçoit des fonctions en paramètres
  function callIt(aFunction) {
    aFunction();
  }

  // Higher Order => ordre supérieur => renvoie des fonctions
  function create() {
    return function(param){
      …
    }
  }

  // Examples d’utilisation
  callIt(f); // fonction stockée précédemment dans f
  callIt(function(){
    …
  }); // Fonction anonyme crée à la volée
  create()(« Nice param »);

Ceux qui suivent auront reconnu dans l’exemple de fonction create le concept vu au début en lambda calcul, dissertant sur l’arité des fonctions à plusieurs paramètres. L’un des grands jeux du programmeur fonctionnel est de justement repérer les structures algorithmiques similaires pour en extraire des abstractions – parfois très très abstraites… – réutilisables.

Le simple fait de pouvoir générer, renvoyer et passer des fonctions à la volée ouvre énormément de possibilités. Ce concept permet à lui seul de remplacer / simuler plusieurs designs-patterns objets impératifs : strategy, factory, template method, command…

Conclusion

A travers quelques exemples simples et un peu de théorie, vous voilà maintenant introduit aux rudiments de la programmation fonctionnelle en Javascript.

La fonction est notre outil principal, et un bon algorithme se doit d’être structuré non en termes de classes s’échangeant des messages et autre changement d’état, mais en termes de combinaison de fonctions. Ces boîtes noires se combinent, s’appellent en se passant et renvoyant des informations, et en ne travaillant que localement, sans jamais altérer une valeur.

A ce stade, des questions subsistent : comment se passer complètement de variables ? Comment alléger une syntaxe qui peut sembler parfois plus verbeuse que la programmation objet ? Autant de sujets que nous aborderons parmi d’autres, dans un prochain article !

Expert technique Java

0 commentaires

votre commentaire

Se joindre à la discussion ?
Vous êtes libre de contribuer !

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Inscription newsletter

Ne manquez plus nos derniers articles !