LIFWEB TP3 - Programmation fonctionnelle en JavaScript
Exercice 0 (PRÉPARATION) : décorateur mystère
Un décorateur est une fonction qui modifie le comportement d’une autre fonction. Plus précisément, c’est une fonction deco qui prend une fonction fct en argument, telle que deco(fct) est la fonction fct dont le comportement est modifié.
On considère le décorateur m
suivant :
function m(f) {
function w(...a) {
w.x.push(a);
return f(...a);
}
w.x = [];
return w;
}
const adder_decorated = m((a, b) => a + b);
console.log(adder_decorated(2, 3));
console.log(adder_decorated(3, 4));
- Expliquer ce que fait ce décorateur.
- Renommer les identifiants de variables, fonctions et propriétés
m
,f
,w
,a
etx
pour rendre le code clair. - Expliquer l’intérêt de ce décorateur pour du test.
Exercice 1 : modificateur de taille
On souhaite modifier tp3.html et tp3.js pour créer des boutons qui modifieront la taille d’une citation (élément HTML <q>
). Il faut que quand on clique sur le bouton Ajouter modificateur de taille et que le champ de saisi vaut n, un nouveau bouton soit ajouté à la page, et que quand on clique sur ce bouton, celui-ci mettra la citation à la taille n lue à la création.
Il faut faire en sorte que la taille soit associée au bouton au moment de sa création : si on lit dynamiquement le contenu de l’élément <input>
, l’effet ne sera pas bon, car tous les boutons redimensionneront à la même taille de fonte !
Ici, on recommande de faire du handler une fermeture qui lie la valeur à laquelle redimensionner. Il y a d’autres solutions possibles, comme associer la taille au bouton avec une data-*
attribute.
Exercice 2 : API fonctionnelle de la classe Array
La classe Array
dispose des méthodes suivantes qui prennent toutes une fonction en paramètre : find, some, every, filter, map, reduce.
On dispose d’un tableau d’objets comme suit :
const database = [
{ name: "Alice", age: 40, sal: 2500 },
{ name: "Bob", age: 17, sal: -1 },
{ name: "Charlie", age: 30, sal: 1800 },
{ name: "Denise", age: 12, sal: -1 },
];
En utilisant l’API Array
, sans utiliser de boucle explicite et avec uniquement des variables const
, écrire les fonctions suivantes qui prennent toutes en paramètre un tableau d’objets comme database
.
- Retourner
true
ssi toutes les personnes majeures ont un salaire supérieur à 1500.- Valeur attendue sur l’exemple :
true
- Valeur attendue sur l’exemple :
- Retourner un tableau de chaines de la forme
nom: age
ne contenant que les personnes majeures.- Valeur attendue sur l’exemple :
[ 'Alice: 40', 'Charlie: 30' ]
- Valeur attendue sur l’exemple :
- Calculer le salaire moyen des personnes majeures, avec un
reduce
qui compte la somme des salaires et en parallèle le nombre de majeurs et une division finale.- Valeur attendue sur l’exemple :
2150
- Valeur attendue sur l’exemple :
- Calculer l’écart type des salaires des personnes majeurs avec un
reduce
. On pourra utiliser pour cela une équation entre la moyenne des écarts quadratiques et la moyenne des carrés pour éviter de calculer la moyenne.- Valeur attendue sur l’exemple :
350
- Valeur attendue sur l’exemple :
Notons que l’usage de reduce
n’est pas toujours recommandé. Notons que pour renvoyer un objet avec une arrow, il faut mettre des parenthèses autour de l’expression x => ({})
sinon c’est la fonction au corps vide x => {}
. Notons enfin que cet exercice ressemble assez au TD5 de LIFPF.
Exercice 3 : le décorateur chrono
On donne l’exemple suivant, d’un décorateur qui affiche le temps d’exécution :
function chrono(fct) {
// c.f. https://javascript.info/rest-parameters-spread
return function (...args) {
const start = Date.now();
// const result = fct(...args);
const result = fct.call(this, ...args);
const end = Date.now();
console.info(`${fct.name}(...) executed in ${end - start}ms`);
return result;
};
}
function sum(array) {
return array.reduce((a, b) => a + b, 0);
}
// pas d'équivalent de range() Python en JS
// le hack suivant permet de créer un tableau [0, 1, ..., 1e7 - 1]
const test = [...Array.from({ length: 1e7 }).keys()];
console.log(sum(test));
const sumChrono = chrono(sum);
console.log(sumChrono(test));
setTimeout(() => sumChrono(test), 1000);
- Tester le décorateur
chrono
sur la fonctionsum
, expliquer ce qu’il fait. - Quand on déclenche
sumChrono
au bout d’une seconde, quel sera le temps affiché parchrono
? Le prévoir avant de tester.
Exercice 4 : décorateurs fonctionnels
Écrire les décorateurs suivants en vous inspirant de chrono
et les tester. On ne demande pas de gérer les exceptions.
once(fct)
: au premier appel, renvoie le résultat renvoyé parfct
et renvoie ce même résultat, quels que soient les arguments passés en paramètres, pour les appels suivants (sans rappelerfct
).- Commencer avec
fct
unaire, puis avec les Rest parameters and spread syntax, passez au cas général.
- Commencer avec
maybe(fct, def)
: appellefct
et sifct
renvoieundefined
, alors renvoiedef
à la place, une valeur par défaut.- Commencer avec
fct
unaire, puis avec les Rest parameters and spread syntax, passez au cas général. - En utilisant les closure, faire en sorte que
fct
ne soit bien exécutée qu’une seule fois.
- Commencer avec
memoize(fct)
: sifct
(supposée unaire) a déjà été appelée avec le même argument, renvoie directement la valeur retournée précédemment parfct
pour cet argument sans exécuter à nouveaufct
.- Ce décorateur ressemble assez à
once(fct)
, avec un dictionnaire pour enregistrer plusieurs valeurs au lieu d’une seule, utiliser Map. - ⚠️ il est assez compliqué de gérer des paramètres non-primitifs (tableaux, objet) ou un nombre arbitraire de paramètres
- Ce décorateur ressemble assez à
chain(n)(fct)
enchaine la fonctionfct
(supposée unaire) n fois, c’est-à-dire renvoie la fonction x ↦ fct(fct(…(fct(x))…)) où fct est appelée n fois. Si l’entier n est nul alors la fonction identité est renvoyée x ↦ x. On donnera une version itérative et une version récursive dechain(n)(fct)
.
Le décorateur once(fct)
est utilisé pour obtenir une sorte de singleton fonctionnel. Quant à, memoize(fct)
il est utilisé comme système de cache pour les fonctions au coût élevé. Notons que cet exercice ressemble assez au TD4 de LIFPF.
Exercice 5 (BONUS) : 🥷 style
Reprendre les décorateurs maybe
et chain
précédents en style ninja avec comme contrainte que chaque décorateur soit défini en une seule ligne (one-liner 🇬🇧).
Exercice 6 (BONUS) : traceur de fonctions récursives
Définir un décorateur tracer(fct)
qui exécute la fonction récursive fct
en traçant les appels récursifs et en indentant lors de chaque appel. On l’utilisera comme suit :
let recFact = (n) => (n === 0 ? 1 : n * recFact(n - 1));
recFact = tracer(recFact);
console.log(recFact(5));
// affiche
// recFact(5)
// recFact(4)
// recFact(3)
// recFact(2)
// recFact(1)
// recFact(0)
// 120
Quel est le comportement quand on omet la ligne recFact = tracer(recFact);
de l’extrait précédent et pourquoi ?
Exercice 7 (DIFFICILE) : décorateur throttle
Le décorateur appelé throttle(fct, ms)
est très utilisé : il assure que la fonction fct
est appelée au plus une fois par ms millisecondes. On l’utilise par exemple sur les champs de recherche, pour éviter de solliciter trop souvent le service au coût (d’entrées/sorties ou de calcul) élevé.
On trouvera une version détaillée de cet exercice sur https://javascript.info/task/throttle dont on reprend l’extrait suivant :
const f1000 = throttle(function f(a) {
console.log(a);
}, 1000);
f1000(1); // 1
f1000(2); // throttling f(2), 1000 not out yet
f1000(3); // throttling f(3), 1000 not out yet
setTimeout(() => {
f1000(4); // shows 4
f1000(5); // throttling f(5), 1000 not out yet
}, 1200);
// affiche
// 1
// throttling f(2), 1000 not out yet
// throttling f(3), 1000 not out yet
// 4
// throttling f(5), 1000 not out yet
Réaliser ce décorateur. On utilisera un timer avec setTimeout(). Dans ce décorateur, la fonction f
est typiquement asynchrone. Si elle ne l’est pas, on l’appellera avec setTimeout(() => funct.call(this, ...args), 0);
avec un délai de 0
pour la rendre asynchrone. Les fonctions console.warn
étant synchrones, on utilisera la même astuce pour que l’ordre des affichages soit celui des appels. On peut réaliser une variante, où si un appel est throttled, quand le délai sera épuisé, on exécutera effectivement l’appel qui a été reporté, les appels précédents étant en revanche eux perdus.