Programmation fonctionnelle avec javascript

Hassane Moustapha
6 min readJul 17, 2017

--

Note: Nous couvrons dans cet article : les fonctions pures, les Higher Order Functions, la composition de fonctions, des curried functions et des applications partielles.

D’abord! Qu’est ce que la programmation fonctionnelle ?

Bonne question!

La programmation fonctionnelle est un paradigme de programmation de type déclaratif qui considère le calcul en tant qu’évaluation de fonctions mathématiques.

Comme le changement d’état et la mutation des données ne peuvent pas être représentés par des évaluations de fonctions, la programmation fonctionnelle ne les admet pas, au contraire elle met en avant l’application des fonctions, contrairement au modèle de programmation impérative qui met en avant les changements d’état. Définition de wikipedia

Pour moi, la programmation fonctionnelle est un peu plus que ça. Une façon élégante de résoudre les problèmes avec des outils et techniques à la portée de tous.

Les fonctions

Dans ce paradigme l’unité de traitement est appelée Fonction. Tout doit se faire avec des fonctions. Ces fonctions sont classées dans deux grandes familles: fonctions pures et fonctions impures.

Une fonction est dite pure si son résultat dépend uniquement des paramètres qu’elle prend en entrée. Une fonction pure ne doit pas altérer l’environnement dans lequel elle est exécutée.

Les fonctions sont considérées comme des données. Elles peuvent être passées à d’autres fonctions comme paramètres et elles peuvent être retournées comme résultats de fonctions.

Exemple:

let somme = (x, y) => {
return x + y;
}

Le résultat de la fonction somme dépend uniquement des valeurs x et y. En plus de cela la fonction somme ne change pas l’environnement dans lequel elle est exécutée contrairement à la fonction suivante:

let somme2 = (x, y) => {
console.log("la somme est ");
return x + y;
}

En utilisant console.log(“la somme est”) la fonction somme2 altère l’environnement. La fonction somme2 peut être considérée comme impure car elle ne respecte pas les deux règles:

1 — le résultat dépend uniquement des paramètres.

2 — ne pas changer l’environnement.

Avantage des fonctions pures

Les fonctions pures ont pour avantage d’être prédictibles. Ce qui permet de les tester plus facilement et surtout de mettre leur résultat en cache pour ne pas avoir à refaire le calcul pour des valeurs qu’on a déjà traitées. Les fonctions pures sont souvent utilisées pour générer d’autres fonctions. Dans ce cas, elles sont appelées “Higher Order Functions” ou “Fonctions de rang supérieur”.

Note: Une fonction de rang supérieur peut ne pas être une fonction pure.

Exemple:

let sumV1 = (x) => (y) => x + y;let sumV2 = function(x){
return function(y){
return x + y;
}
}

Note : sumV1 et sumV2 sont équivalentes mais écrites avec deux syntaxes différentes.

Elles ( sumV1 et sumV2 ) permettent de recevoir une valeur x puis elles retournent une fonction qui attend un y et cette dernière retourne la somme de x et y.

Ok mais ça sert à quoi ?

Ben… c’est joli non ?

Et c’est très utile. Supposons que nous avons besoin d’une fonction qui permet d’incrémenter une valeur donnée et lui ajoute 1. On pourra utiliser cette fonction sumV1 pour régler ce problème.

let plus1 = sumV1(1);plus1(99); // 100 

Plus sérieusement … supposons que nous avons des données à traiter. Chose normale je suppose. Mais le traitement des données que nous voulons faire peut changer à tout moment en fonction du client en face.

Exemple: Nous avons une liste de tâches (todos) et nous voulons les trier suivant la date d’échéance. Nous pouvons faire :

let todos = JSON.parse( "mes_todos_qui_viennent_du_backend.json" );let dateVoulue = new Date();todos.filter( todo => todo.date === dateVoulue );
// ou bien
let todoDescriptif = todo => todo.date === dateVoulue;
todos.filter(todoDescriptif);

Les deux versions/méthodes marchent mais la seconde est un peu plus élégante et surtout plus facile à lire et comprendre car elle cache les détails de l’opération et elle est plus facile à configurer. Exemple:

let filtrerParDate = (laDate) => (todo) => todo.date === laDate;
let pourAujourdhui = filtrerParDate( new Date() );
// option 1
todos.filter( pourAujourdhui );
// ou bien // option 2
todos.filter( filtrerParDate( new Date() );

Personnellement je préfère l’option 1. Elle est plus propre et plus élégante.

Petite parenthèse

La fonction filtrerParDate aurait pu être écrite de cette manière. La manière plus ou moins standard.

let filtrerParDate = function( uneDate, unTodo){
return unTodo.date === uneDate;
}

Mais nous avons préféré de l’utiliser sous sa forme curried. Le “currying” est : transformer une fonction qui prend N arguments en N fonctions qui prennent chacune UN SEUL argument. L’intérêt des fonctions qui prennent un seul argument est qu’elles peuvent être combinées ou composées pour créer de nouvelles fonctions. Nous verrons des exemples dans la suite de cet article.

Revenons à notre mission: filtres ces todos. Après avoir donné la possibilité de filtrer les todos par DATE, on nous demande maintenant de pouvoir les filtrer par UTILISATEUR.

let filtrerParUtilisateur = (user_id) => (todo) => todo.user_id === user_id

Super. C’était rapide et très simple mais …..

let filtrerParUtilisateur = 
(user_id) => (todo) => todo.user_id === user_id
let filtrerParDate =
(laDate) => (todo) => todo.date === laDate;

Les deux fonctions se ressemblent un peu trop. On dirait qu’elles sortent du même moule. Alors … créons ce moule.

Les deux fonctions ont le même corps : objet.attribut === valeur ce qui veut dire que notre moule doit pouvoir construire ces fonctions en se basant sur 3 paramètres : Un Objet, Un Attribut et Une Valeur. L’ordre dans lequel ces arguments sont passés au moule est très important car nous cherchons à créer des prédicats ( fonctions retournant True ou False ) basés sur un Attribut dont le contenu est comparé à Une Valeur donnée puis l’objet contenant cet attribut. Ce qui donne un moule de cette forme :

let moule = (attr) => (val) => (obj) => obj.attr === val;// qui peut être écrit sous une autre formelet moule = (attr) => (val) => (obj) => obj[attr] === val;

Utilisons alors notre moule pour créer nos filtres.

let filtrerParDate = moule('date');
let pourAjourdhui = filtrerParDate( new Date() );
todos.filtrer( pourAujourdhui );let filtrerParUtilisateur = moule('user_id');
let pourToto = filtrerParUtilisateur(10098);
todos.filter( pourToto );

Nous avons utilisé notre moule pour générer nos prédicats et savons maintenant que les Higher Order Functions peuvent être super utiles.

Encore une très petite parenthèse

Utiliser une curried function F est une opération appelée application partielle de F.

let filtrerParDate = moule('date');

filtrerParDate est une application partielle de la fonction moule.

Revenons à nos … fonctions.

Combiner des fonctions

Maintenant que nous savons filtrer nos todos par Date ou par Utilisateur on veut pouvoir les filtrer par Date et par Utilisateur. Chose déjà faite … ou presque. Nous avons tous les éléments dont on a besoin pour réaliser cette fonctionnalité.

let _date = DATE_VENUE_D_AILLEURS;let parDate = moule('date')( _date_ );
let parUtilisateur = moule('user_id')(10098);

let parDateEtParUtilisateur = (todo) => {
return parDate(todo) && parUtilisateur(todo);
}

Hummmmm. Ok ça marche et c’est pas moche. Pas du tout. Mais nous aimerions rendre ce processus plus cool. On veut pouvoir créer les combinaisons à la volée et ainsi pouvoir créer des filtres basés sur d’autres critères et règles métier qui viendront plus tard. Revoyons alors notre code et créons la fonction qui rendra possible ce changement.

let combineFunctions = (fonctionF, fonctionG) => (todo) => {
return fonctionF(todo) && fonctionG(todo);
}

Utilisons maintenant cette fonction pour avoir le comportement souhaité.

let parDateEtParUtilisateur = 
combineFunctions(parDate, parUtilisateur);
todos.filter( parDateEtParUtilisateur );

C’est super cool de pouvoir écrire un code propre et qui fait exactement ce que nous voulons et de manière simple et flexible.

A ce stade, nous avons la possibilité de créer des filtres à la volée, les utiliser dans notre application ou les combiner pour créer d’autres filtres plus complexes selon le besoin.

En plus de la simplicité, l’élégance ( parfois ) et la flexibilité, la programmation fonctionnelle nous montre qu’il y a souvent d’autres façons de résoudre les problèmes informatiques.

--

--

Hassane Moustapha

Independent consultant. I love Rust, Ruby, Go, Elixir, Haskell, Elm and …javascript