Impératif vs Fonctionnel : Fight !

Salut ! Ca fait un moment que je n’ai plus écrit d’articles ici mais je compte bien m’y remettre avec pas mal de choses sur la programmation fonctionnelle et Scala 😉

Il y a quelques jours j’ai participé à NCrafts et en particulier au workshop Playing with projections de Michel Grootjans et Thomas Coopman; je l’ai trouvé génial et je vous le recommande sans réserve !!!
Son but est d’analyser une liste d’événements d’une application afin d’en extraire différentes informations (projections dans le jargon). Pour cela, on se base sur les événements d’une application où des joueurs peuvent s’enregistrer, créer des quiz puis y répondre (schéma global des événements).
Mais, dans cet article, je vais détourner son but et m’en servir pour comparer le style impératif et fonctionnel en essayant d’avoir le code le plus clair et intuitif à chaque fois (au passage, si vous voyez des améliorations je suis preneur ^^).
Et le tout en JavaScript 🙁 puisque c’est le langage que nous avions choisi lors du workshop…

Bon, commençons très simple, la première tâche est de déterminer le nombre total d’inscriptions. Pour cela, il suffit de compter les événements PlayerHasRegistered, le code est donc plutôt trivial.

Avec le style impératif il faut donc juste un for et un if :

Du côté fonctionnel, encore plus simple, un simple filter fait l’affaire :

Quand on compare les deux codes, a différence la plus visible est bien sûr le nombre de lignes : 1 vs 7 ! C’est déjà un gros point mais, le plus important est que le code fonctionnel reflète bien plus l’intention initiale (compter les événements PlayerHasRegistered).
On peut considérer ça comme un détail mais du code court et expressif facilite grandement la compréhension du code 🙂
Bien sûr, il faut connaître la fonction filter mais elle est très simple et son nom est plutôt explicite… Pour ceux qui ne la connaitraient pas, elle créée simplement un nouveau tableau à partir de l’ancien en conservant uniquement les éléments pour lesquels la fonction passée en paramètre renvoi true. Si elle n’existait pas en JavaScript, on pourrait la coder très simplement :

On voit ici qu’elle est très similaire au code impératif écrit précédemment… En fait, la fonction filter (comme celles qu’on verra ensuite) permet de définir un comportement habituel (ici, filtrer un tableau) qui pourra être réutilisé à volonté. Plus besoin de le coder à chaque fois \o/

Par ailleurs, le code fonctionnel reflète bien les deux étapes de calcul : filtrer les événements puis les compter, alors que le code impératif fait tout en même temps. Ce qui semble plus « logique » au regard du résultat attendu mais tends à complexifier le code (même si ici ça reste tout à fait acceptable ^^).
Sur un exemple aussi simple que ça, on voit déjà que le code fonctionnel encourage à séparer un algorithme en sous-parties (éventuellement réutilisables) alors que le code impératif encourage à créer du code spécifique qu’il faudra réécrire à chaque fois (notamment pour des comportement très communs comme le filter).
Enfin, on peut noter que la fonction filter prends une autre fonction en paramètre, c’est ce qu’on appelle une fonction d’ordre supérieur (buzzword powaaa).

Passons maintenant à l’étape suivante, nous voulons faire la même chose mais pour chaque mois et renvoyer une tableau avec le mois et le nombre d’inscriptions. On ne peut donc plus se contenter d’un simple compteur :

Ici, le code est déjà plus complexe, on doit gérer autant de compteurs qu’il y a de mois et ensuite formater le résultat comme attendu. Lire et comprendre le code est déjà beaucoup moins évident et on imagine facilement quelques endroits où pourraient se glisser des bugs…

Du côté fonctionnel en revanche le code se complexifie très peu :

Même si on ne comprends pas exactement ce que font les méthodes, grâce à leur nom on imagine bien l’intention voulue 🙂
Encore une fois, la comparaison est sans appel, autant sur la taille (4 lignes vs 19) que sur la lisibilité. On commence aussi à voir que le style impératif est susceptible d’accueillir bien plus de bugs…

Bon, je dois quand même avouer qu’il manque encore en JavaScript certaines fonctions de base que j’ai dû rajouter :

La fonction groupBy groupe les éléments d’un tableau en fonction d’une clé (chaîne de caractère) et renvoit une Map (objet JavaScript) qui a pour valeur la liste des éléments ayant la même clé.
La fonction map existe sur les tableaux (elle permet de créer un nouveau tableau en modifiant chaque élément) mais pas sur les Map (objets JavaScript). Je l’ai donc ajoutée pour permettre d’obtenir un tableau à partir des valeurs de la Map (et éventuellement la clé, en deuxième paramètre).

Nous venons de résoudre les deux premiers challenges du workshop et on a bien vu que le code impératif se complexifie bien plus vite que le code fonctionnel. Voyons ce que ça peut donner avec le challenge suivant… On doit lister les 10 quiz les plus populaires avec leur id, nom et nombre de fois qu’ils ont été joués.
Regardons d’abord le code fonctionnel cette fois-ci :

Encore une fois, on voit bien que le style fonctionnel permet d’identifier clairement les différentes étapes de l’algorithme : on prend les jeux qui ont été ouverts et démarrés et on les groupe par quiz. Ensuite il ne reste plus qu’à formater les données en ajoutant le titre, les trier puis garder uniquement les 10 premiers.

Plutôt direct, non ?

Je vous laisse comparer avec la version impérative :

On commence maintenant à avoir l’habitude, le code fonctionnel est une fois de plus beaucoup plus court (forcément puisque pas mal de code est intégré dans les fonctions map, filter, groupBy…). On peut aussi remarque que le code fonctionnel est plutôt déclaratif, on décrit les intentions (filtrer, transformer, agréger…) sans définir comment le faire (le comment est géré par les implémentations des fonctions mapfiltergroupsBy…), ce qui permet éventuellement de les modifier sans toucher au code écrit.

Par exemple, voici l’implémentation fonctionnelle du deuxième challenge en Scala :

Si on souhaite paralléliser les traitements, il suffit d’ajouter .par :

Ou pour rendre l’exécution lazy, on remplace simplement les List par des Stream :

Bref, le code change très peu (y compris entre JavaScript et Scala) alors qu’on a des exécutions très différentes.

De plus, on voit bien que beaucoup de bugs / fautes d’inattention peuvent se glisser dans le code impératif alors que le code fonctionnel, reste bien plus concis, clair et bien moins sujet à erreurs; et ce, d’autant plus si on bénéficie d’un bon système de types 😉
Essayez d’introduire un bug dans le code Scala qui ne soit pas identifié par le compilateur…

On a principalement vu ici les fonctions de chaînage sur les collections et c’est clairement celles-ci qui m’ont fait accrocher à la programmation fonctionnelle mais ce n’est qu’un petit bout pour démarrer… Je pense que ces quelques exemples ont bien illustré mon point mais j’aimerais beaucoup avoir votre avis sur ce que vous en avez pensé…

Pour ceux qui le souhaitent, voici l’implémentation complète des challenges basiques du workshop en fonctionnel et en impératif.

Enfin, pour terminer voici un petit exercice, comment coderiez-vous la fonction take (qui prends les n premiers éléments d’un tableau ou tout le tableau s’il est plus petit) ?

12 réponses à “Impératif vs Fonctionnel : Fight !

  1. Anon

    Les exemples de fonctionnel me font penser à jQuery et ses méthodes de chaining. On peut dire que c’est du fonctionnel ou aucun rapport ?

    • Hello, pour jQuery c’est plutôt le pattern « Fluent API » où on retourne systématiquement le « this » afin de pouvoir chaîner les fonctions.
      Le fait de chaîner les fonctions (ce qui est souvent appréciable) n’est pas forcément révélateur de fonctionnel ou d’objet même si en fonctionnel c’est très souvent le cas.
      Là où on voit l’approche fonctionnelle c’est avec les fonctions d’ordre supérieur (qui prennent en paramètre une autre fonction), avec le fait qu’aucune fonction ne modifie les paramètres ou son objet (on renvoi toujours de nouveaux tableaux/objets)…

  2. Merci pour cette introduction.

    Attention toutefois au monkey-patching des prototypes de Array ou Object qui pourrait nous mordre les doigts plus tard.

    • En effet ^^
      C’est clairement pour illustrer mes propos sur le fonctionnel (ces fonctions existent dans d’autres langages) mais c’est vraiment à éviter pour du code de prod.
      Pour du code en prod, il vaut mieux utiliser lodash même si c’est un peu moins sympa niveau syntaxe… (j’aurais pu/du le préciser dans l’article, merci pour ton commentaire).

      • Maarek Joseph

        Tu peux essayer lodash/fp pour une syntaxe encore plus fonctionnelle.

        https://github.com/lodash/lodash/wiki/FP-Guide

        • Oui ou http://ramdajs.com/docs/ 😉
          Mais je vois pas bien comment chaîner facilement les méthodes… Faudrait que je creuse un peu…

          • Maarek Joseph

            Ici tu peux voir un article qui explique la différence entre le chainage de fonction lodash, et l’utilisation de lodash/fp

            En principe cela est equivalent:

            La seule chose qui compte pour pouvoir enchainé un traitement dans le flow, c’est qu’on y insère une fonction qui prend en paramètre le résultat de l’opération précédente et qui renvoi une nouvelle fonction qui travaille sur ce résultat pour renvoi un nouveau résultat. Et si il n’y a pas d’opération précédente, le résultat initiale sera la collection passé en paramètre à flow.

            https://medium.com/making-internets/why-using-chain-is-a-mistake-9bc1f80d51ba

          • Cool 🙂
            Merci pour les exemples et l’article !

  3. Paul

    J’aime de plus en plus la programmation fonctionnelle, elle permet d’écrire moins de code plus lisible, mais souvent l’optimisation n’est plus au rendez vous ou alors elle devient moins évidente à faire.
    Et on se retrouve avec des applications/siteweb qui sont aussi lent que ceux des années 90/2K alors que nos ordinateurs sont beaucoup plus puissant.

    Dans votre exemple « registeredPlayersPerMonth » dans la partie impérative on voit vient qu’on parcours 2 listes, une fois chacune, alors que dans la version fonctionnelle on perd cette visibilité et dans votre algorithme, on parcours 2fois les events (une fois pour filtrer et une fois pour grouper).

    Après ca dépends du langage aussi, car il me semble qu’en Java8 avec les stream et les lambda, les opérations sont séquentielle (ie: 1 seul parcours de liste)

    • Salut Paul, en effet, généralement le code fonctionnel est un peu moins performant que le code impératif, c’est une sort de compromis 😉
      En revanche, un code moins performant n’implique pas nécessairement une application moins performante. En général, la lenteur d’une application vient d’un goulot d’étranglement local (base de données, web service, algorithme avec une grande complexité…). Et bien sûr, sur les parties très critiques niveau performance, mieux vaut rester sur de l’impératif voir sur des langages plus bas niveau…
      Pour ton exemple des sites lents aujourd’hui, je ne pense pas que ce soit dû à la programmation fonctionnelle (assez peu doivent l’utiliser) mais plutôt à la taille des pages… ^^
      Bref, c’est un sujet compliqué qu’il vaut mieux voir au cas pas cas mais personnellement je ne pense pas que ce soit un obstacle à la programmation fonctionnelle.

      J’ai fait quelques mesures de performance rapide et voici les résultats :
      registredPlayers: 2 ms en impératif vs 3 ms en fonctionnel
      registredPlayersPerMonth: 7 ms en impératif vs 17 ms en fonctionnel
      popularQuizs: 8 ms en impératif vs 26 ms en fonctionnel
      On voit bien que plus la complexité monte, plus le fonctionnel perd en performances (c’est normal on ajoute des étapes supplémentaires).
      Après, comme tu le dis, certains langages/librairies peuvent optimiser ça en fusionnant certaines opérations…

  4. […] (Source: Journal du hacker) […]

  5. Bonjour,
    Merci pour cet article ! Il est vrai qu’il est parfois difficile de débattre entre le fonctionnel et le fonctionnel.

Répondre à Anon Annuler la réponse

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

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">