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 et x 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
  • 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' ]
  • 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
  • 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

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 fonction sum, expliquer ce qu’il fait.
  • Quand on déclenche sumChrono au bout d’une seconde, quel sera le temps affiché par chrono ? 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é par fct et renvoie ce même résultat, quels que soient les arguments passés en paramètres, pour les appels suivants (sans rappeler fct).
  • maybe(fct, def) : appelle fct et si fct renvoie undefined, alors renvoie def à 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.
  • memoize(fct) : si fct (supposée unaire) a déjà été appelée avec le même argument, renvoie directement la valeur retournée précédemment par fct pour cet argument sans exécuter à nouveau fct.
    • 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
  • chain(n)(fct) enchaine la fonction fct (supposée unaire) n fois, c’est-à-dire renvoie la fonction x ↦ fct(fct(…(fct(x))…))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 de chain(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.