17 septembre 2020 Nicolas Delauney

Parole d’expert : la programmation fonctionnelle

La programmation fonctionnelle, encore un buzz word ? Pas forcément ! Dans cette parole d’expert, nous vous expliquons tout sur cette philosophie et pourquoi vous devriez (peut-être) l’utiliser.

Kézako ?

La programmation fonctionnelle est une manière de penser son code, de manière déclarative (opposée à la manière impérative). Dans le premier cas, on donne une suite d’instructions à appliquer pour parvenir au résultat, dans le second on change l’état du programme.

La programmation fonctionnelle est souvent opposée à la programmation orientée objet, bien que certains concepts soient applicables n’importe où, comme nous le verrons par la suite. Tous les principes reposent plus sur une logique mathématique que sur un problème de langage, et les exemples de cet article sont en typescript mais auraient pu être écrits dans n’importe quel autre langage.

Principes de base

Dans le cas de la programmation fonctionnelle, l’unité de base est une fonction. Et pas n’importe laquelle : une “fonction pure”. Qu’est-ce que c’est ? Simplement une méthode qui ne modifie pas ses variables d’entrée, et qui retournera toujours le même résultat pour une même entrée.

Conséquence : il est très facile de tester et débugger une fonction pure, puisqu’elle ne dépend pas de l’état du programme. De plus, un bon nommage permet souvent de réduire la complexité du code et de faciliter la lecture.

Comme nous l’avons dit, l’unité de base est une fonction. Mais cette fonction peut prendre en entrée une autre fonction (décorateur), et même retourner une fonction (currying)!

Décoration et currying

La décoration permet d’extraire un comportement commun à plusieurs méthodes qui à priori n’ont ni le même rôle ni les mêmes responsabilités. Le currying est quand à lui plus rarement rencontré comme dans cet exemple avec la succession de parenthèses car il n’apporte pas beaucoup plus de lisibilité. Par contre il permet de facilement créer des fonctions “filles” paramétrisées comme ce qui suit :

currying

Facilité de lecture

Bon jusque là, tous les exemples étaient plutôt simplistes. On se retrouve rarement à coder une addition… Voici donc une petite étude de cas pour vous montrer l’argument majeur de la programmation fonctionnelle : la lisibilité. Mais attention ! Un grand pouvoir implique de grandes responsabilités, et une bonne lisibilité est avant tout garantie par un bon nommage : dans la mesure du possible, le nom de la fonction doit suffire à comprendre ce qu’elle retourne.

# Les protagonistes

Pour cet exemple, nous voulons pouvoir connaître la température moyenne d’un département, à l’aide d’un ensemble de capteurs autorisés. Chaque capteur possède donc une ville, le numéro de son département, une température, et son état (actif ou non).

Modèle des senseurs

# Une première approche

Le problème peut être résolu en une seule fonction relativement simple :

Une première approche

Elle répond bien à la question, et en plus en point bonus c’est déjà une fonction pure ! Mais par contre, on la lit difficilement. Notre cerveau butte sur chaque mot pour l’interpréter. La répétition du mot sensor perturbe la lecture, puisque l’on doit à chaque fois être sûr(e) de notre emplacement (dans le filtre ou dans le map?)

Découpage en briques fonctionnelles

Avec une approche fonctionnelle, la lecture est beaucoup plus rapide :

Approche par programmation fonctionnelle

On comprend l’intention de chaque ligne, sans être noyé sous des détails inutiles. Dans un contexte de maintenabilité du code, on détecte d’autant plus rapidement le problème que l’on n’a pas besoin de lire et décrypter tout le code avant d’arriver à la source du problème. On réduit aussi les problèmes d’indentations qui peuvent polluer la lecture. On pourra, ensuite et au besoin, aller voir l’implémentation de chacune des méthodes, que voici:

L'envers du décors

Mais… Pourquoi on s’embête alors ? Cette méthode est beaucoup plus verbeuse, on a besoin de 13 lignes contre seulement 9 au début ! Une fois encore, le gain se fait sur le long terme. Imaginons que 6 mois plus tard on vous demande la moyenne de température mais de tous les capteurs . La première méthode amènera forcément de la duplication de code. Ou à un refacto plus important pour extraire cette duplication. Alors qu’avec l’approche fonctionnelle, tout est déjà là, prédécoupé, prêt à être modifié. En l’occurence, le .filter(enabledAndInDepartment(department)) devient .filter(isEnabled).

Les plus pointilleux diront qu’il y a quand même un peu de duplication de code, et je leur répondrai qu’on pourra alors créer un décorateur qui prendra comme argument la fonction à effectuer dans le filtre. La signature de nos deux fonctions deviendrait alors :

Refactoring

Un monde parfait ?

Évidemment non, si tout était parfait tout le monde l’utiliserait depuis bien longtemps. Il faut savoir faire la part des choses entre réusabilité et lisibilité. Le dernier snippet introduisant getAverageTempWithFilter complexifie inutilement la lisibilité, pour corriger une duplication de seulement 3 lignes. De plus, suivant les cas métiers une fonction pure n’est pas forcément le choix idéal, on mixera donc les implémentations avec des fonctions impures ou des méthodes d’objets pour la POO. Enfin, je dois vous confesser un léger mensonge : les langages imposent une certaine limite à ce qu’il est possible de faire. Vous aurez remarqué que la plupart des méthodes utilisées dans les exemples sont des variables, introduites par le mot clé const, et non des fonctions (déclarées par le mot clé function). Dans le détail, ça peut avoir son importance, en terme de déclaration, d’instanciation, etc.

En bref, la programmation fonctionnelle met à disposition une série de trucs et astuces pour clarifier et simplifier le code. Cela assure une meilleure testabilité et une plus grande modularité, et donc, un code propre et maintenable. À utiliser sans attendre !