Camembert > Programmation fonctionnelle
[C'est] un paradigme de programmation qui considère le calcul en tant qu'évaluation de fonctions mathématiques.
It is a declarative programming paradigm, which means programming is done with expressions.
Sources : Wikipedia FR / Wikipedia EN
Slides sympa : The Lambda Calculus and The JavaScript
Apprendre Haskell permet d'écrire de meilleurs programmes, même dans d'autres langages.
Aujourd'hui on va explorer des concepts vraiment cools issus de la programmation fonctionnelle moderne :
et les appliquer en JS !
function diviserParDeux(nombre) {
var missile = lancerUnMissileNucleaire();
fairePorterLeChapeauAuDrManhattan(missile);
return nombre / 2;
}
Cette fonction a des effets de bord observables qu'on ne peut pas deviner en regardant sa valeur de retour. Il faut donc faire constamment attention lorsqu'on la manipule !
function ajouterCinq(nombre) {
return nombre + 5;
}
Cette fonction n'a pas d'effets de bord. Elle renvoie toujours le même résultat lorsqu'on l'appelle avec les mêmes arguments.
ajouterCinq(-4) === ajouterCinq(-4)
Le résultat du programme ne change pas si on remplace une expression par une expression de valeur équivalente.
Utiliser des fonctions pures permet de raisonner sur son programme comme sur une équation.
La logique métier est fiable, sans surprise et aisément testable.
On sait quelles fonctions ont des effets de bord, ce qui permet d’être prudent lorsqu’on les manipule.
En JS, les fonctions sont des objets de première classe (first-class citizen).
Une fonction d’ordre supérieur est une fonction qui possède au moins l'une des propriétés suivantes :
function appliquerDeuxFois(f, x) {
return f(f(x));
}
// ^ accepte une fonction en paramètre
function foisTrois(x) {
return x * 3;
}
appliquerDeuxFois(foisTrois, 7);
// --> (7 * 3) * 3 = 63
On va voir un exemple concret : les opérations sur les tableaux en JS.
var villes = [
{ nom: "Nantes", dep: 44, mer: false },
{ nom: "Dunkerque", dep: 59, mer: true },
{ nom: "Paris", dep: 75, mer: false },
];
On veut obtenir une liste de chaines de type ville (dep)
.
On va écrire une fonction rendreVillesAffichables
telle que :
rendreVillesAffichables(villes);
// --> ["Nantes (44)", "Dunkerque (59)",
// "Paris (75)"]
function rendreVillesAffichables(villes) {
for (var i = 0; i < villes.length; i++) {
villes[i] = villes[i].nom +
" (" + villes[i].dep + ")";
}
return villes;
}
On mélange 2 comportements :
function rendreVillesAffichables(villes) {
function transformation(ville) {
return ville.nom +
" (" + ville.dep + ")";
}
return villes.map(transformation);
}
Array.map
gère le parcours du tableau \o/Avec les lambdas et les template strings c'est encore plus clair !
villes.map(v => `${v.nom} (${v.dep})`);
// --> ["Nantes (44)", "Dunkerque (59)",
// "Paris (75)"]
villes.filter(function(item) {
// Si la ville est près de la mer,
// on renvoie true, sinon false
return item.mer;
});
// --> [ { nom: "Dunkerque", dep: 59,
// mer: true }
villes.filter(function(item) {
return !item.mer;
}).map(function(item) {
return item.nom + " (" + item.dep + ")";
});
// --> ["Nantes (44)", "Paris (75)"]
Rappel : Array.map
et Array.filter
sont des fonctions d'ordre supérieur.
var personnes = [
{ nom: "Bruce", age: 30 },
{ nom: "Tony", age: 35 },
{ nom: "Peter", age: 26 },
];
personnes.reduce(function(acc, cur) {
return acc + cur.age;
}, 0);
// --> 91
function map(tableau, transformation) {
return tableau.reduce(function(acc, cur) {
acc.push(transformation(cur));
return acc;
}, []);
}
function filter(tableau, predicat) {
return tableau.reduce(function(acc, cur) {
if (predicat(cur)) acc.push(cur);
return acc;
}, []);
}
Éviter d'utiliser des boucles pour manipuler des tableaux.
Si vous avez un tableau et que vous voulez :
strict evaluation, eager evaluation, greedy evaluation
non-strict evaluation, lazy evaluation
L'exécution d'un bout de code ne se fait pas avant que les résultats de ce bout de code ne soient réellement nécessaires.
A JavaScript utility library delivering consistency, modularity, performance, & extras.
var t = [0, 1, 2, 3, 4];
function plusUn(nb) {
console.log(nb + ' + 1');
if (nb > 2) console.log('Traitement long');
return nb + 1;
}
function petit(nb) {
console.log(nb + ' plus petit que 3 ?');
return nb < 3;
}
var js = t
.map(plusUn)
.filter(petit)
.slice(0, 2);
0 + 1
1 + 1
2 + 1
3 + 1
Traitement long
4 + 1
Traitement long
1 plus petit que 3 ?
2 plus petit que 3 ?
3 plus petit que 3 ?
4 plus petit que 3 ?
5 plus petit que 3 ?
[ 1, 2 ]
var _ = require('lodash');
var lodash = _(t)
.map(plusUn)
.filter(petit)
.take(2)
.value();
0 + 1
1 plus petit que 3 ?
1 + 1
2 plus petit que 3 ?
[ 1, 2 ]
Avantages : peut augmenter la maintenabilité et les performances
Inconvénients : peut introduire de l'overhead (dépend pas mal de la techno)
Un objet immuable est un objet dont l'état ne peut pas être modifié après sa création.
var n = 10;
n = 10 + 1;
On a juste changé la référence (n
), pas les données (10
)
10 + 1
ne modifie ni 10
ni 1
var objet = {
a: 1,
b: 'BATMAN',
};
var alias = objet;
objet.a = 2;
objet // { a: 2, b: 'BATMAN' }
alias // { a: 2, b: 'BATMAN' }
const objet = {
a: 1,
b: 'BATMAN',
};
const alias = objet;
objet.a = 2;
objet // { a: 2, b: 'BATMAN' }
alias // { a: 2, b: 'BATMAN' }
const
empêche de modifier la référence, pas la valeur !
const
c'est top quand même ;)
Immutable persistent data collections for Javascript which increase efficiency and simplicity.
https://facebook.github.io/immutable-js/
var Immutable = require('immutable');
var map1 = Immutable.Map({a:1, b:2, c:3});
var map2 = map1.set('b', 50);
map1.get('b'); // 2
map2.get('b'); // 50
var map3 = map2.set('b', 2);
map1.equals(map3); // --> true
Attention : ne pas confondre Map
(structure de données) et map
(fonction) !
Et les perfs ?
Introduit de l'overhead, mais souvent le compromis maintenabilité/performances est bon
const
Et let
le reste du temps. Pas var
.
Une méthode qui modifie un objet devrait renvoyer un nouvel objet, pas le modifier en cachette.
const tableau = [1, 2, 3];
tableau.push(4);
// ^ beurk un effet de bord
Twitter : @d_sferruzza
Slides sur GitHub :