Coderstand

PF: Les fonctions pures

tags: programmation fonctionnelle

08/09/2017 (c'était un vendredi)

Rappel du sommaire

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.


Brice Coquereau

Bonjour! Je suis Brice Coquereau, parisien, développeur, flexitarien, et amateur de chats. Je vais à des meetups assez souvent. Venez me parler sur Mastodon ou Twitter (mais Mastodon c'est mieux)

TwitterFacebookLinkedin