PF: Les fonctions pures
tags: programmation fonctionnelle
08/09/2017 (c'était un vendredi)
Rappel du sommaire
- Les fonctions en tant que paramètres
- L’immutabilité
- Les fonctions pures <--- vous êtes ici
- L’application partielle
So pure, so wow
Une fonction pure est une fonction:
- qui n’utilise que des variables passées en paramètres,
- sans effets de bord,
- qui ne modifie pas de variables extérieures,
- ni les variables passées en paramètre.
L’avantage principal d’une fonction pure est que l’appel à cette fonction avec les même paramètres renverra toujours le même résultat. On simplifie également la mise en place des tests automatiques, ce qui sécurise notre application.
Détaillons les 4 points de cette liste.
Qui n’utilise que des variables passées en paramètres
Prenons un exemple concret
let promo = 20;
const calculeReduction = (prix) => {
return prix * promo / 100;
}
calculeReduction(100) // 20
// 100 lignes plus loin, une autre fonction modifie 'promo'
promo = 25;
calculeReduction(100) // 25
La fonction calculeReduction
est impure, car elle utilise la variable promo
, qui n’a pas été passée en paramètre.
Est-ce grave? On ne dirait pas. Mais comment être sûr que la fonction fait bien son travail?
Comment tester facilement notre fonction?
On se retrouve à devoir mettre en place un environnement où promo a été pré-remplie,
tout ça pour tester une bête fonction mathématique.
test() {
// D'abord je remplis promo
promo = 20;
// Ensuite je teste
assert(calculeReduction(100)).equal(20);
// On fait du code sécurisé, on teste 2 cas.
promo = 30;
assert(calculeReduction(100)).equal(30);
}
Mais on s’en fiche des tests, on fait des vraies applications nous!
C’était un cas simple. Imaginez le même scénario dans une application multi-threadé,
-
avec le thread 1 qui met
promo
à 20, -
puis qui se fait couper par thread 2, qui met
promo
à 40, -
puis thread 1 fait son calcul.
Bravo, vous venez d’accorder 40% de réduction à un client qui devait en avoir 20.
Si on transforme notre code en fonction pure, on obtient
let promo = 20;
const calculeReduction = (prix, promo) => {
return prix * promo / 100;
}
calculeReduction(100, promo) // 20
// 100 lignes plus loin, une autre fonction modifie 'promo'
promo = 25;
calculeReduction(100, promo) // 25
Et là, on peut tester facilement que calculePrixReduit(100,20) renvoie bien 20.
test() {
assert(calculeReduction(100,20)).equal(20);
}
il n’y a plus de ‘magie’ extérieure, tout est passé en paramètre!
Sans effet de bord
Un effet de bord est un effet dont nous ne sommes pas sûr du résultat. Par exemple:
- une requête réseau. Le serveur que j’appelle peut être coupé. Ou déconnecté temporairement. Ou en cours de maintenance. Bref, je ne suis pas sûr que ma requête se passera bien.
- une écriture dans un fichier. Est-ce que le disque est plein? Est-ce que j’ai les droits d’écrire dans ce fichier?
- un appel à Math.random(). C’est dans le nom de la fonction!
Cette fois-ci, on code un jeu de dé
// Valeur entrée par le joueur
const valeurJoueur = 4;
const jeu = (valeurDuJoueur) => {
const hasard = Math.ceil(Math.random()*6);
if(hasard === valeurDuJoueur) {
return true;
} else {
return false;
}
}
// Et on joue!
jeu(valeurJoueur) // Renvoie true ou false.
Super jeu! Maintenant, testons notre fonction, pour vérifier que les gens ne gagnent pas à chaque fois.
test() {
assert(jeu(4)).equal(true);
}
Est-ce que ce test fonctionne? La plupart du temps, non, parce que le hasard fait son oeuvre. Je ne peux pas avoir un test qui validera à chaque fois que ma logique de jeu n’est pas détraquée.
Rendons cette fonction pure.
// Valeur entrée par le joueur
let valeurJoueur = 4;
const jeuPur = (valeurDuJoueur, valeurDuDe) => {
if(valeurDuDe === valeurDuJoueur) {
return true;
} else {
return false;
}
}
// Et on joue!
let hasard = Math.ceil(Math.random()*6);
jeuPur(valeurJoueur, hasard) // Renvoie true ou false.
Et le test devient
test() {
assert(jeu(4, 4)).equal(true);
assert(jeu(4, 3)).equal(false);
}
Mon test est écrit, mon code est sécurisé, et je suis sûr que le joueur ne gagnera pas à chaque coup.
Qui ne modifie pas de variables extérieures, ni les variables passées en paramètre.
Si la fonction pure modifie des variables extérieures ou passées en paramètre, on prend le risque d’impacter d’autres parties de notre code à cause de notre fonction
Par exemple, si on reprend le cas du calculateur de promo, rien ne garantit que la variable promo
n’est pas lue ailleurs.
Si une fonction impure modifie une variable extérieure
let promo = 20;
let produit = {
nom: 'T shirt',
prix: 100
};
const calculeReduction = (produit, promotion) => {
return produit.prix * promotion / 100;
};
const fonctionImpure = () => {
promo = 100; // On modifie une variable extérieure
};
calculeReduction(produit, promo); // 20
fonctionImpure();
calculeReduction(produit, promo); // 100
On voit que l’appel à une fonction impure peut modifier le fonctionnement d’une fonction pure, car on a modifié une variable extérieure.
On a les même perturbations avec la modification d’une variable passée en paramètre
let promo = 20;
let produit = {
nom: 'T shirt',
prix: 100
};
const calculeReduction = (produit, promotion) => {
return produit.prix * promotion / 100;
};
const fonctionImpure = (produit) => {
produit.prix = 50; // On modifie la variable passée en paramètre
};
calculeReduction(produit, promo); // 20
fonctionImpure(produit);
calculeReduction(produit, promo); // 10
Ici, fonctionImpure
a modifié mon produit, sans que la modification soit clairement explicité, et cela impacte la suite du code.
Si on ré-écrit notre fonction pour qu’elle soit pure.
let promo = 20;
let produit = {
nom: 'T shirt',
prix: 100
};
const calculeReduction = (produit, promotion) => {
return produit.prix * promotion / 100;
};
const fonctionPure = (produit) => {
// On renvoie un nouvel objet, qui est la copie de `produit`
return {
...produit
prix: 50; // mais avec un prix différent
};
};
calculeReduction(produit, promo); // 20
const newProduit = fonctionPure(produit);
calculeReduction(produit, promo); // toujours 20
calculeReduction(newProduit, promo); // 10
Maintenant, le fait de récupérer un nouveau produit est explicite,
et le code qui suit l’appel à fonctionPure
montre clairement si on utilise produit
ou nouveauProduit
,
il n’y a plus d’ambiguité.
Si on mélange fonctions pures et variables immutables…
On obtient un code beaucoup plus simple à appréhender, car aucune variable n’est modifiée au cours de sa vie, et nous sommes assurés que les fonctions appelées n’auront pas d’incidence sur les autres parties de l’application. On peut donc avoir une vraie séparation entre les couches de code, avec l’assurance qu’il n’y aura pas de dépendances cachées.
Conclusion
Même s’il est impossible d’écrire une application réelle avec uniquement des fonctions pures, on peut isoler des blocs traitements fonctionnels dans des fonctions pures, ce qui nous garantit que la logique la plus importante de notre application sera facilement testable et maintenable.