Je vais vous parler d'un sujet qui me tient à coeur, les fonctions pures.
Les fonctions pures sont les blocs de construction atomiques en programmation fonctionnelle. Elles sont adorées pour leur simplicité et leur testabilité.
Ce post couvre une liste de contrôle rapide pour savoir si une fonction est pure ou non.
La Liste de Contrôle
Une fonction doit passer deux tests pour être considérée comme "pure" :
- Mêmes entrées donnent toujours les mêmes sorties
- Pas d'effets de bord
Zoomons sur chacun d'eux.
1. Même Entrée => Même Sortie
Comparez ceci :
const add = (x, y) => x + y; add(2, 4); // 6
À cela :
let x = 2;
const add = (y) => {
x += y;
};
add(4); // x === 6 (la première fois)
Fonctions Pures = Résultats Cohérents
Le premier exemple retourne une valeur basée sur les paramètres donnés, peu importe où/quand vous l'appelez.
Si vous passez 2 et 4, vous obtiendrez toujours 6.
Rien d'autre n'affecte la sortie.
Fonctions Impures = Résultats Incohérents
Le deuxième exemple ne retourne rien. Il repose sur un état partagé pour faire son travail en incrémentant une variable en dehors de sa propre portée.
Ce modèle est le cauchemar des développeurs.
L'état partagé introduit une dépendance temporelle. Vous obtenez des résultats différents selon le moment où vous appelez la fonction. La première fois donne 6, la prochaine fois 10 et ainsi de suite.
Quelle Version Est Plus Facile à Comprendre ?
Laquelle est moins susceptible de générer des bugs qui ne se produisent que dans certaines conditions ?
Laquelle est plus susceptible de réussir dans un environnement multi-thread où les dépendances temporelles peuvent casser le système ?
Définitivement la première.
2. Pas d'Effets de Bord
Ce test est en lui-même une liste de contrôle. Quelques exemples d'effets de bord sont :
- Muter votre entrée
- console.log
- Appels HTTP (AJAX/fetch)
- Changer le système de fichiers (fs)
- Interroger le DOM
En gros, tout travail qu'une fonction effectue qui n'est pas lié au calcul de la sortie finale.
Voici une fonction impure avec un effet de bord.
Pas Si Mauvais
const impureDouble = (x) => {
console.log('doubling', x);
return x * 2;
};
const result = impureDouble(4);
console.log({ result });
console.log est l'effet de bord ici mais, en pratique, cela ne nous fera pas de mal. Nous obtiendrons toujours les mêmes sorties, étant donné les mêmes entrées.
Cela, cependant, peut causer un problème.
Changer "Impurement" un Objet
const impureAssoc = (key, value, object) => {
object[key] = value;
};
const person = {
name: 'Bobo'
};
const result = impureAssoc('shoeSize', 400, person);
console.log({
person,
result
});
La variable, person, a été changée pour toujours parce que notre fonction a introduit une instruction d'affectation.
L'état partagé signifie que l'impact de impureAssoc n'est plus entièrement évident. Comprendre son effet sur un système implique maintenant de retrouver chaque variable qu'il a touchée et de connaître leur historique.
État partagé = dépendances temporelles.
Nous pouvons purifier impureAssoc en retournant simplement un nouvel objet avec les propriétés souhaitées.
Purifier
const pureAssoc = (key, value, object) => ({
...object,
[key]: value
});
const person = {
name: 'Bobo'
};
const result = pureAssoc('shoeSize', 400, person);
console.log({
person,
result
});
Maintenant pureAssoc retourne un résultat testable et nous ne nous inquiéterons jamais s'il a muté quelque chose ailleurs en silence.
Vous pourriez même faire ce qui suit et rester pur :
Une Autre Façon Pure
const pureAssoc = (key, value, object) => {
const newObject = { ...object };
newObject[key] = value;
return newObject;
};
const person = {
name: 'Bobo'
};
const result = pureAssoc('shoeSize', 400, person);
console.log({
person,
result
});
Muter votre entrée peut être dangereux, mais muter une copie de celle-ci ne pose aucun problème. Notre résultat final est toujours une fonction testable et prévisible qui fonctionne peu importe où/quand vous l'appelez.
La mutation est limitée à cette petite portée et vous retournez toujours une valeur.
Clonage Profond des Objets
Attention ! Utiliser l'opérateur de propagation ... crée une copie superficielle d'un objet. Les copies superficielles ne sont pas à l'abri des mutations imbriquées.
Mutation Imbriquée Non Sûre
const person = {
name: 'Bobo',
address: { street: 'Main Street', number: 123 }
};
const shallowPersonClone = { ...person };
shallowPersonClone.address.number = 456;
console.log({ person, shallowPersonClone });
Les deux person et shallowPersonClone ont été mutés parce que leurs enfants partagent la même référence !
Mutation Imbriquée Sûre
Pour muter en toute sécurité des propriétés imbriquées, nous avons besoin d'un clonage profond.
const person = {
name: 'Bobo',
address: { street: 'Main Street', number: 123 }
};
const deepPersonClone = JSON.parse(JSON.stringify(person));
deepPersonClone.address.number = 456;
console.log({ person, deepPersonClone });
Maintenant, vous êtes garanti de la sécurité car ce sont vraiment deux entités distinctes !
Petite liste de contrôle
- Une fonction est pure si elle est exempte d'effets de bord et retourne la même sortie, étant donné la même entrée.
- Les effets de bord incluent : muter l'entrée, appels HTTP, écriture sur disque, impression à l'écran.
- Vous pouvez cloner en toute sécurité, puis muter, votre entrée. Laissez simplement l'original intact.
- La syntaxe de propagation (… syntaxe) est le moyen le plus simple de cloner superficiellement des objets.
- JSON.parse(JSON.stringify(object)) est le moyen le plus simple de cloner profondément des objets.