LIFWEB - CM3

Programmation Fonctionnelle et Asynchrone

Romuald THION

Semestre printemps 2023-2024 UCBL

Programmation fonctionnelle en JS

Concepts de la programmation fonctionnelle

Glossary of Modern JavaScript Concepts (2017)

  • Purity: pure functions, side effects,
  • State: stateful and stateless,
  • Immutability and mutability,
  • Imperative and declarative programming,
  • Higher-order functions, …

Voir LIFPF - Programmation Fonctionnelle.

Programmation Fonctionnelle Aujourd’hui

Fonctions en JS

Portée des variables

☠️ var : portée fonction ☠️

⚠️ let/const : portée lexicale (bloc) ⚠️

👍 let/const, la portée de i est la boucle for.

for (let i = 0; i < 3; i++) {
  console.log(i); // 0, then 1, then 2
}
console.log(i); // Uncaught ReferenceError: i is not defined

👎 var, la portée de i est la fonction englobante.

for (var i = 0; i < 3; i++) {
  console.log(i); // 0, then 1, then 2
}
console.log(i); // affiche 3 😱

Déclaration de fonction

// déclaration de funct
function funct(arg1, arg2, ..., argN) {
  // instructions
  return expression;
}

// appel
const res = funct(p1, p2, ..., pN);

Ici, on crée une fonction avec une instruction (statement 🇬🇧) function qui ne produit pas de valeur de retour (MDN).

Expression fonctionnelle

// NFE affectée à funct
const funct = function (arg1, arg2, ..., argN) {
  // instructions
  return expression;
};

// appel
const res = funct(p1, p2, ..., pN);

👉 Ici, on crée une fonction comme une expression avec laquelle on initialise une variable funct, c’est une Named Function Expression - (NFE) (javascript.info, MDN).

Expression fléchée (1/3)

A.k.a., arrow functions, fat arrows ou lambdas (MDN).

// arrow avec bloc d'instructions et return explicite
const funct = (arg1, arg2, ..., argN) => {
  // instructions
  return expression;
};

// appel
const res = funct(p1, p2, ..., pN);

⚠️ Sans return, funct renvoie undefined. ⚠️

Expression fléchée (2/3)

// arrow affectée à funct
const funct = (arg1, arg2, ..., argN) => expression;

// appel
const res = funct(p1, p2, ..., pN);

Quasi-équivalente à la NFE suivante :

let funct = function(arg1, arg2, ..., argN) {
  return expression;
};

Et à l’expression du λ-calcul :

\lambda a_1 \ldots \lambda a_n . e

Expression fléchée (3/3)

⚠️ NFE et arrows ne sont pas équivalentes, car une arrow a this dans sa fermeture statique, mais pas la NFE qui l’obtient dynamiquement. ⚠️

Arrow functions do not have this. If this is accessed, it is taken from the outside. (Arrow functions, the basics et Arrow functions revisited)

Inside a function, the value of this depends on how the function is called. Think about this as a hidden parameter of a function - just like the parameters declared in the function definition, this is a binding that the language creates for you when the function body is evaluated. (MDN)

🪳 Si vous utilisez this dans un handler avec une arrow, this ne sera pas l’élément auquel on attache le handler. 🪳

const fNFE = function (x) {
  return console.log(`this=${this} id=${this.id} x=${x}`);
};
const fArrow = (x) => console.log(`this=${this} id=${this.id} x=${x}`);

const object = { id: 42, fNFE, fArrow };
object.fNFE(3); // this = object
object.fArrow(3); // this = globalThis

const testButton = document.querySelector("#test");
testButton.addEventListener("click", fNFE); // this = testButton
testButton.addEventListener("click", fArrow); // this = globalThis
testButton.click();

Voir exemple.

this=[object Object] id=42 x=3
this=[object Window] id=undefined x=3
this=[object HTMLButtonElement] id=test x=[object MouseEvent]
this=[object Window] id=undefined x=[object MouseEvent]

Les fermetures (closures)

Terme utilisé en JS pour (\lambda x.e) v : l’expression e dans laquelle x prend la valeur à v.

function hello(person) {
  return function (content) {
    return `Hi ${person}, ${content}`;
  };
}

const helloBuddy = hello("Buddy");
const helloGuys = hello("Guys");
console.log(helloBuddy("c'mon."));
console.log(helloBuddy("welcome."));
console.log(helloGuys("how do you do?"));

💡 helloBuddy() et helloGuys() sont des fermetures (closures 🇬🇧) où person est liée. 💡

En λ-calcul (avec \mathsf{++} la concaténation) :

\begin{align*} \mathsf{hello} &\equiv \lambda p . \lambda c . p \mathsf{++} c\\ \mathsf{helloBuddy} &\equiv (\mathsf{hello}\; \mathtt{Buddy})\\ &\equiv (\lambda p . \lambda c . p \mathsf{++} c)\; \mathtt{Buddy}\\ &\equiv (\lambda c . p \mathsf{++} c)[p := \mathtt{Buddy}] \end{align*}

\begin{align*} \mathsf{helloBuddy}\; \mathtt{OK} &\equiv ((\lambda c . p \mathsf{++} c)[p := \mathtt{Buddy}])\mathtt{OK} \\ &\equiv (p \mathsf{++} c)[p := \mathtt{Buddy}, c := \mathtt{OK}] \end{align*}

💡 (\lambda c . p \mathsf{++} c)[p := \mathtt{Buddy}] est une fermeture :

  • une expression fonctionnelle (\lambda c . p \mathsf{++} c)
  • munie d’un environnement [p := \mathtt{Buddy}] qui fait correspondre des valeurs aux variables
  • revoir LIFLC - CM6 et LIFPF - CM1.

Ici le calcul termine effectivement avec (p \mathsf{++} c)[p := \mathtt{Buddy}, c := \mathtt{OK}] évalué en \mathtt{Buddy} \mathsf{++} \mathtt{OK}.

📖 La sémantique de JS est informelle ECMA-262, sa formalisation se construit a posteriori, e.g. KJS: A Complete Formal Semantics of JavaScript - PLDI’15, voir aussi TypeScript.

Exemple : fermeture avec le DOM (1/2)

function fabriqueRedimensionneur(taille) {
  return function () {
    // 🚨 taille est capturée 🚨
    document.body.style.fontSize = taille + "px";
  };
}

const taille12 = fabriqueRedimensionneur(12);
const taille14 = fabriqueRedimensionneur(14);
document.querySelector("#t-12").onclick = taille12;
document.querySelector("#t-14").onclick = taille14;

💡 La variable taille est liée.

Exemple : fermeture avec le DOM (2/2)

La fermeture de M. Jourdain (TP2) :

const $anchors = document.querySelectorAll("#images-list li > a");
const $display = document.querySelector("#image-container");

for (const $a of $anchors) {
  $a.addEventListener("click", function clickButton(event) {
    // 🚨 $a est capturée car portée block 🚨
    event.preventDefault();
    const $img = document.createElement("img");
    $img.src = $a.href;
    $display.replaceChildren($img);
  });
}

Exemple : tableau de fonctions

const fsMap = [1, 2, 3].map((x) => (y) => y + x);
console.log(fsMap.map((f) => f(1)));
// [ 2, 3, 4 ] 👍

const fsConst = [];
for (const a of [1, 2, 3]) fsConst.push((x) => x + a);
console.log(fsConst.map((f) => f(1)));
// [ 2, 3, 4 ] 👍

const fsVar = [];
for (var a of [1, 2, 3]) fsVar.push((x) => x + a);
console.log(fsVar.map((f) => f(1)));
// [ 4, 4, 4 ] 🤯

Voir l’exemple.

🤔 let/const n’ont été introduits qu’avec ES6 / ECMAScript 2015, comment faisait-on depuis 1995 ?

IIFE

An IIFE (Immediately Invoked Function Expression) is a JavaScript function that runs as soon as it is defined MDN.

(function () {
  var x1 = ... ;
  var x2 = ... ;
  /* … */
  return ...
})();

📜 Permet de limiter la portée des variables avec une closure : méthode avant la standardisation des modules ESM (ES6) pour encapsuler.

💡 Les IIFEs permettent de résoudre le problème du tableau de fonctions avec var.

const functs = [];
for (var a of [1, 2, 3])
  (function (y) {
    functs.push((x) => x + y);
  })(a); // IIFE

console.log(functs.map((f) => f(1)));
// [ 2, 3, 4 ] 😌

Ici, on introduit y pour capturer a via l’IIFE :

(\lambda x. x + y)[y := a]

Style objet ou fonctionnel ?

Closures and classes behave differently in JavaScript with a fundamental difference: closures support encapsulation, while JavaScript classes don’t support encapsulation.

Closures offer simplicity, since we don’t have to worry about the context that this is referring to. (How to decide between classes v. closures in JavaScript)

Exemple : compteur

Class, style ES6+

class CountClass {
  constructor() {
    this.count = 0;
  }
  up() {
    return ++this.count;
  }
}
// ❗ new crée un nouvel objet
const obj1 = new CountClass();
const obj2 = new CountClass();
console.log(obj1.up()); // 1
console.log(obj1.up()); // 2
console.log(obj2.up()); // 1

Closure, style FP

function countClosure() {
  // closure
  let count = 0;

  return () => ++count;
}

// ❗ pas de new
// ❗ pas de this crée
const funct1 = countClosure();
const funct2 = countClosure();
console.log(funct1()); // 1
console.log(funct1()); // 2
console.log(funct2()); // 1

Voir class-vs-closure.js.

📝 LIFWEB n’est pas tout à fait une UE sur JavaScript et les subtilités de son modèle objet : on sera utilisateurs des objets et on évitera this en privilégiant le style fonctionnel. ⚠️

Programmation asynchrone

Programmation événementielle \approxeq asynchrone

La boucle d’événements

Boucle d’événéments Source : JavaScript Visualized: Event Loop

JavaScript Visualizer 9000

https://jsv9000.app/

Exemple : les handlers onevent

const $output = document.querySelector("#output");
const $input = document.querySelector("#input");
const $eval = document.querySelector("#eval");

function work() {
  // 💡 ici, $input.value n'est pas dans la closure
  // la variable n'est PAS dans la fermeture
  $output.textContent += `${2n ** BigInt($input.value)}\n`;
}
$eval.addEventListener("click", work);

Voir l’exemple.

Exemple : les alarmes

console.log("Start"); // (A)
setTimeout(
  // (T1)
  () => console.log("Call back #1"), // (CB1)
);
console.log("Middle"); // (B)
setTimeout(
  // (T2)
  () => console.log("Call back #2"), // (CB2)
);
console.log("End"); // (C)

🧠 setTimeout(fct, delay=0, ...params) rend fct asynchrone : les alarmes sont déclenchées au début du tour suivant de la boucle d’événements.

❓ Qu’est-ce qui s’affiche dans la console ? (exemple)

Attention au code bloquant avec les alarmes

const s = new Date().getSeconds();
setTimeout(function () {
  console.log(`Elapsed: ${new Date().getSeconds() - s} seconds`);
}, 0);

while (true) {
  if (new Date().getSeconds() - s >= 2) {
    console.log("Good, looped for 2 seconds");
    break;
  }
}

⚠️ Ici, l’alarme sera déclenchée après au moins deux secondes (exemple).

💀 Ne réalisez pas vous-même cette cascade.

Fonctions asynchrones

  • 🗒️ Les fonctions synchrones sont bloquantes :
    • le thread d’exécution attend la fin du traitement
  • 💡 Les fonctions asynchrones ne sont pas bloquantes
    • Le thread d’exécution continue après l’appel.
    • On passe en paramètre la suite du traitement :
      • Paramètre appelé handler ou callback.
    • 🔁 Le traitement sera déclenché ultérieurement :
      • dans un autre tour de la boucle d’événements.

Voir sur javascript.info, MDN ou Node.js.

Omniprésence de l’asynchrone en JS

Passage de paramètres et retour asynchrone

function work(x) {
  console.info(`work(${x})`);
  return x ** 2;
}

💡 setTimeout(fct, delay=0, ...params)

  • fct devient asynchrone ;
  • fct doit être une fonction ;
  • exécution de fct(...params) après delay ms.

👎 Ce n’est pas la meilleure façon de rendre une fonction asynchrone. On en verra d’autres.

Passage de paramètre

On veut exécuter work(2) en asynchrone.

// 🔥 typeof(work(2)) == "number"
setTimeout(work(2), 0);
// ❎ pas de paramètre
setTimeout(work, 0);

// 👎 spécifique à setTimeout
setTimeout(work, 0, 2);

// 👌 fonction explicite
setTimeout(function () {
  return work(2);
}, 0);
// 👌 fat arrow
setTimeout(() => work(2), 0);

🚨 Comment obtenir la valeur de retour de work(n) ?

How do I return the response from an asynchronous call? - SO

Tentative 1 : avec return

function work(x) {
  console.info(`work(${x})`);
  return x ** 2;
}

function asyncWork(n) {
  return setTimeout(() => work(n), 3000);
}

const r1 = asyncWork(12);
console.log(r1);

❎ Ici, r1 est l’identifiant de l’alarme obtenue de façon synchrone, pas le retour de work(n) (exemple) !

Tentative 2 : avec callback

function work(x) {
  console.info(`work(${x})`);
  return x ** 2;
}

function asyncWork(n, callback) {
  return setTimeout(() => {
    const r = work(n);
    callback(r);
  }, 3000);
}

const r1 = asyncWork(3, (r) => console.log(r));
console.log(r1);

✅ Ici, r1 est toujours l’alarme, mais on a bien exécuté (r) => console.log(r) avec la valeur de retour de work(n) après 3000 ms (exemple) !

Tentative 3 : enchainer les traitements

Phénomène appelé pyramid of doom ou callback hell

function asyncWork(n, callback) {
  return setTimeout(() => callback(n ** 2), 3000);
}

asyncWork(3, (r1) => {
  console.log(r1);
  asyncWork(r1, (r2) => {
    console.log(r2);
    asyncWork(r2, (r3) => console.log(r3));
  });
});

✅ Ici, on parvient à chaîner les asyncWork, mais il faut imbriquer des fonctions (exemple) !

Problème : pyramid of doom (ou callback hell)

Les promesses et async/await résoudront ce problème (image).

Conclusion

  • 🗒️ Asynchrone = exécution de fonction dans le futur
    • Ajout à la queue sur déclenchement d’un event.
    • Puis traitement par l’event loop.
  • ⚠️ return renvoie une valeur synchrone :
    • ❎ ne peut pas renvoyer une valeur future :
      • seulement une promesse de valeur.
    • ✅ permet d’interrompre le flux d’exécution :
      • comme throw, mais pour un succès.
  • 💡 On doit passer la suite du traitement en paramètre.
    • Principe du Continuation Passing Style (CPS).