S'il y a une chose qu'il faut avoir bien compris, c'est le concept de fonction pure.
Une fonction pure est une fonction qui au regard des mêmes entrées fournit toujours la même sortie et le fait sans aucun effet de bord visible.
Prenez slice
et splice
. Ces deux fonctions font exactement la même chose - d'une façon bien
différente, je vous l'accorde, mais néanmoins la même chose. On dit que slice
est pure car
elle retourne toujours la même sortie pour une même entrée, c'est garanti. splice
en revanche
grignotera volontiers une partie de la liste d'entrée et l'altérera à jamais; c'est un effet de
bord observable.
var xs = [1,2,3,4,5];
// pure
xs.slice(0,3);
//=> [1,2,3]
xs.slice(0,3);
//=> [1,2,3]
xs.slice(0,3);
//=> [1,2,3]
// impure
xs.splice(0,3);
//=> [1,2,3]
xs.splice(0,3);
//=> [4,5]
xs.splice(0,3);
//=> []
En programmation fonctionnelle, nous n'aimons pas les fonctions complexes telles que splice
qui modifient (mutate) les entrées. Cela ne convient pas; nous recherchons des fonctions de
confiance qui nous retournerons toujours le même résultat, pas des fonctions qui laisseront
derrière elles un fatras d'effets peu désirables (comme splice
).
Jetons un oeil à l'exemple suivant.
// impure
var minimum = 21;
var checkAge = function(age) {
return age >= minimum;
};
// pure
var checkAge = function(age) {
var minimum = 21;
return age >= minimum;
};
Dans la portion impure, checkAge
dépend d'une variable mutable minimum
afin de déterminer
le résultat. En d'autres termes, la fonction dépend de l'état courant du système et introduit
dans le même temps un environnement externe difficile à considérer.
Cela peut sembler anodin dans cet exemple mais pouvoir se fier ainsi à l'état des variables est
une condition nécessaire à l'élaboration de systèmes complexes
[^http://www.curtclifton.net/storage/papers/MoseleyMarks06a.pdf]. Selon ses entrées la fonction
checkAge
peut retourner des résultats différents ce qui d'une part n'est pas conforme à la
notion de pureté mais qui d'autre part, force votre esprit à être en alerte dès lors qu'une
modification minime doit s'opérer sur le programme.
Dans sa forme pure en revanche, la fonction est totalement autonome et hermétique. Il est aussi
possible de rendre minimum
immuable, renforçant dans le même temps la pureté de la fonction.
Pour ce faire, créons un objet à geler.
var immutableState = Object.freeze({
minimum: 21
});
Jetons un oeil plus attentif à ces "effets de bord". Quels sont donc ces abominables effets de bord que l'on mentionne dans la définition de fonction pure? Nous désignerons par effet tout ce qui peut arriver au cours d'une exécution en dehors du calcul d'un résultat.
Il n'y a rien d'intrinsèquement mauvais dans les effets si bien que nous les utiliserons bientôt à tout va dans les prochains chapitres. C'est la partie sur le bord qui a mauvaise réputation. L'eau seule n'a rien d'un incubateur à larves, ce sont les parties stagnantes qui créent des marais répugnants, et je vous l'assure, les effets de bord sont en tout point similaire pour votre programme.
Un effet de bord est un changement de l'état du système ou une interaction visible avec le monde extérieur qui se produit lors du calcul d'un résultat.
Ceci inclut mais n'est pas limité à / aux:
- Modifier un fichier du système
- Ajouter une entrée à une base de données
- Effectuer une requête http
- Assignations et changements d'état de variables
- Afficher à l'écran ou dans la console
- Demander une entrée utilisateur
- Accéder au DOM
- Accéder à une information de l'environnement système
La liste continue ainsi de suite. Toutes interactions avec le monde en dehors d'une fonction sont des effets de bord, ce qui vous laisse entrevoir la commodité qu'ils constituent. Toutefois, l'un des postulats de la programmation fonctionnelle stipule que les effets de bord sont généralement la cause de comportements hasardeux.
Il ne s'agit pas de ne pas les utiliser, mais plutôt, d'apprendre à les contenir et à s'en servir de façon contrôlée. Nous aborderons les monades et foncteurs au cours des prochains chapitres cependant pour l'instant, veillons à bien séparer ces fonctions piégeuses des autres plus pures.
Les effets de bord empêchent une fonction d'être pure, ça tombe sous le sens: les fonctions pures par définition doivent nécessairement retourner la même sortie par rapport à des entrées données ce qui ne peut être garanti dès lors que la fonction se repose sur autre chose que son corps local.
Analysons plus attentivement pourquoi nous insistons autant sur cette histoire de même sortie pour une même entrée. Rhabillez-vous, nous partons faire des Mathématiques de haut vol.
Tiré et traduit de mathisfun.com:
Une fonction est une relation privilégiée entre deux valeurs: à chacune de ses valeurs d'entrée est associée une unique valeur de sortie.
En d'autres termes, c'est simplement une relation entre deux valeurs: l'entrée et la sortie.
Néanmoins bien qu'à chaque entrée soit associée une et une seule sortie, cette sortie n'est pas
forcément unique à une entrée donnée. Vous trouverez ci-après un diagramme
d'une fonction tout à fait valide qui à x
associe y
.
[^http://www.mathsisfun.com/sets/function.html]
En comparaison, le diagramme suivant montre une relation qui n'est pas une fonction en ce que
la valeur d'entrée 5
pointe sur plusieurs sorties.
[^http://www.mathsisfun.com/sets/function.html]
Les fonctions peuvent être décrites comme un ensemble de paires (ou couples) de la forme:
(input, output): [(1,2), (3, 6), (5,10)]
[^].
Ou encore sous forme de tableau:
Input | Output |
---|---|
1 | 2 |
2 | 4 |
3 | 6 |
Mais aussi en tant que courbe avec x
comme entrée et y
comme sortie:
Lorsque les entrées impliquent directement les sorties, il n'y a pas lieu de considérer les
détails d'implémentation d'une fonction. N'étant alors qu'un ensemble d'associations d'une
entrée à une sortie, on pourrait imaginer représenter les fonctions à l'aide d'un objet
littéral et les appeler avec []
en lieu et place des habituelles ()
.
var toLowerCase = {"A":"a", "B": "b", "C": "c", "D": "d", "E": "e", "D": "d"};
toLowerCase["C"];
//=> "c"
var isPrime = {1:false, 2: true, 3: true, 4: false, 5: true, 6:false};
isPrime[3];
//=> true
Bien entendu, on préfèrera calculer plutôt que d'écrire à la main chacune des valeurs de sortie. Ceci illustre toutefois une façon connexe de considérer les fonctions. [^]
Maintenant le fin mot de l'histoire: les fonctions pures sont des fonctions au sens mathématique et c'est ce sur quoi se fonde fondamentalement la programmation fonctionnelle. Programmer à l'aide de ces petites bêtes sages apporte d'énormes avantages. Regardons quelques raisons qui justifient notre irrémédiable envie de préserver la pureté de nos fonctions.
En entrée de matière, soulignons que les fonctions pures peuvent mettre en cache un résultat associé à une certaine valeur. Cela se réalise typiquement grâce à une technique appelée memoization.
var squareNumber = memoize(function(x){ return x*x; });
squareNumber(4);
//=> 16
squareNumber(4); // retourne la valeur cachée pour l'entrée 4
//=> 16
squareNumber(5);
//=> 25
squareNumber(5); // retourne la valeur cachée pour l'entrée 4
//=> 25
Bien qu'il existe de nombreuses autres implémentations plus robustes, en voici une relativement modeste:
var memoize = function(f) {
var cache = {};
return function() {
var arg_str = JSON.stringify(arguments);
cache[arg_str] = cache[arg_str] || f.apply(f, arguments);
return cache[arg_str];
};
};
Un autre point intéressant à souligner est qu'il est possible de transformer des fonctions impures en fonctions pures en retardant le moment de leur évaluation:
var pureHttpCall = memoize(function(url, params){
return function() { return $.getJSON(url, params); }
});
Ce qui est intéressant ici c'est que nous ne faisons pas réellement l'appel http - nous
retournons à la place une fonction qui sera à-même de le faire une fois appelée. La fonction
englobante est pure car elle retournera toujours la même fonction d'exécution pour une même
entrée: c'est cette fonction retournée qui s'occupera de réaliser concrètement l'appel http
correspondant à un url
et params
donnés.
Notre fonction memoize
est tout à fait correcte et de fait, ne met pas en cache le résultat
de l'appel http, mais plutôt la fonction générée.
C'est pour l'instant fort peu utile mais nous verrons bientôt quelques astuces de grand-mère qui changeront cela. Ce qu'il faut en retenir, c'est qu'il nous est possible de mettre en cache n'importe quelle fonction, peu importe leur niveau d'impureté.
Les fonctions pures sont totalement autonomes. Tout ce dont la fonction a besoin lui est servi sur un plateau. Arrêtons-nous un instant... En quoi cela est-il favorable? Premièrement, les dépendances d'une fonction sont explicites et par conséquent simple à voir et comprendre - nullement besoin de regarder la machinerie sous le capot.
//impure
var signUp = function(attrs) {
var user = saveUser(attrs);
welcomeUser(user);
};
//pure
var signUp = function(Db, Email, attrs) {
return function() {
var user = saveUser(Db, attrs);
welcomeUser(Email, user);
};
};
L'exemple ci-dessus illustre en quoi une fonction pure se doit d'être honnête au sujet de ses
dépendances de telle façon qu'elles nous apparaissent clairement. Au regard seulement de la
signature, on sait que l'on aura besoin d'une Db
, d'un Email
et d'attrs
.
Nous verrons comment rendre des fonctions de la sorte pure sans pour autant grandement retarder leur évaluation; gardez néanmoins bien à l'esprit que la forme pure est bien plus claire et parlante que son insidieuse homologue impure.
Force est de constater qu'il nous faut également sinon "injecter" les dépendances du moins les passer en paramètres ce qui rend notre application nettement plus flexible: nous avons rendu paramétrables notre base de données et le client mail[^]. Qu'il s'agisse de facilement changer notre système de base de données pour un autre ou encore de réutiliser cette fonction dans une application différente, cette façon de faire y répond sans problème.
Dans le monde du JavaScript, la portabilité peut ne signifier rien de plus que sérialiser et envoyer nos fonctions à travers des sockets. Autrement dit, faire tourner tout le code d'une application dans des workers web. La portabilité est un atout puissant.
Contrairement aux méthodes et procédures "typiques" en programmation fonctionnelle profondément ancrées dans leur environnement d'exécution via des états, dépendances et divers effets, les fonctions pures peuvent s'exécuter partout où le vent les porte.
À quand remonte la dernière fois que vous ayez copié une méthode d'une application à une autre ? L'une de mes citations favorites de l'inventeur d'Erlang, Joe Armstrong est celle ci: "Le problème avec les langages orientés objets c'est qu'il transporte tout un environnement implicite avec eux. Vous vouliez une banane mais vous obtenez un gorille qui tient cette banane... Ainsi que toute la jungle".
En outre, on en vient à réaliser que les fonctions pures rendent le test largement plus facile. Pas besoin de simuler une plateforme de paiement et de présumer de l'état du monde entier après chaque test. On donne juste à la fonction des entrées, et on vérifie sa sortie.
En réalité, de nombreux pionniers de la communauté mettent au point des outils complexes capables de générer des entrées et d'inférer des propriétés que doit posséder la sortie. Tout ceci est bien au-delà de la portée de ce livre mais je vous encourage fortement à rechercher et essayer Quickcheck - un outil de test taillé pour les environnements purement fonctionnels.
Beaucoup pensent que l'un des plus gros points forts des fonctions pures est la transparence référentielle. Un bout de code qui est référentiellement transparent peut se substituer à sa valeur de sortie sans avoir aucun impact sur le comportement du programme.
Comme les fonctions pures retournent toujours la même sortie selon une entrée donnée, on peut compter sur elles pour obtenir un résultat cohérent qui préserve la transparence référentielle. Voyez plutôt:
var Immutable = require("immutable");
var decrementHP = function(player) {
return player.set("hp", player.get("hp")-1);
};
var isSameTeam = function(player1, player2) {
return player1.get("team") === player2.get("team");
};
var punch = function(player, target) {
if(isSameTeam(player, target)) {
return target;
} else {
return decrementHP(target);
}
};
var jobe = Immutable.Map({name:"Jobe", hp:20, team: "red"});
var michael = Immutable.Map({name:"Michael", hp:20, team: "green"});
punch(jobe, michael);
//=> Immutable.Map({name:"Michael", hp:19, team: "green"})
decrementHP
, isSameTeam
et punch
sont toutes trois pures et de fait référentiellement
transparentes. On peut utiliser une technique appelée raisonnement équationnel afin de
substituer des parties équivalentes du code et ainsi raisonner plus facilement sur ce dernier.
C'est comme procéder à une évaluation manuelle du code sans considérer les fioritures
contingentes d'une analyse programmatique. En utilisant la transparence référentielle, jouons
un peu avec le code précédent.
Tout d'abord, développons l'appel à la fonction isSameTeam
.
var punch = function(player, target) {
if(player.get("team") === target.get("team")) {
return target;
} else {
return decrementHP(target);
}
};
Notre structure de données étant immuable, nous pouvons remplacer les équipes par leur valeur courante.
var punch = function(player, target) {
if("red" === "green") {
return target;
} else {
return decrementHP(target);
}
};
En évaluant la condition, on se rend compte que la branche if
est inutile.
var punch = function(player, target) {
return decrementHP(target);
};
En développant decrementHP
, on fait apparaître que dans ce cas précis, un coup de poing n'est
seulement qu'un appel visant à décrémenter les hp
de 1.
var punch = function(player, target) {
return target.set("hp", target.get("hp")-1);
};
Cette aptitude à raisonner sur le code est redoutablement efficace pour refactorer et comprendre le code en général. En fait nous avons déjà utilisé cette technique pour revoir notre programme avec les mouettes. Nous avons utilisé un raisonnement équationnel afin de tirer parti des propriétés de l'addition et du produit. Ainsi, nous serons amenés à utiliser davantage ces techniques tout au long du livre.
Enfin, le coup de grâce, n'ayant aucun effet de bord notable ni besoin d'accès à la mémoire, des fonctions pures peuvent s'exécuter en parallèle plutôt aisément.
C'est tout à fait possible côté serveur à l'aide de threads ou encore, côté client dans les navigateurs à l'aide de workers web bien que la mode actuelle tend à les éviter car jugés trop complexes lorsqu'il s'agit de fonctions impures.
Nous venons de voir ce que sont les fonctions pures et pourquoi en tant que développeurs du monde fonctionnel elles vont constituer le coeur de notre démarche. De fait, à partir de maintenant nous nous efforcerons d'écrire toutes nos fonctions d'une façon pure. Nous aurons besoin d'outils et d'astuces supplémentaires pour le faire cependant que nous apprendrons à bien séparer les parties impures du reste du code plus pur.
Sans davantage d'atouts dans notre manche, écrire des programmes entiers à l'aide de fonctions pures va être un tantinet difficile. Il nous faut ruser et jongler avec des tas d'arguments à passer çà et là; De plus, interdit d'utiliser des états et nul besoin de mentionner tout ce qui touche aux effets de bord. Comment en vient-on à devenir masochiste au point d'écrire de tels programmes ? Il est temps de faire la connaissance de la curryfication.