ES6 en détails est une série d’articles décrivant les nouvelles fonctionnalités ajoutées au langage de programmation JavaScript avec la sixième édition du standard ECMAScript (ES6 en abrégé).

Ravi de vous retrouver pour la suite d’ES6 en détails ! J’espère que vous avez savouré cette petite pause (NdT : qui n’a pas été aussi longue entre les articles traduits ;)). Toutefois, la vie d’un développeur n’est pas rythmée uniquement par le soleil ou la limonade. Il est temps de reprendre où nous nous étions arrêtés. Le sujet de cet article est idéal pour cette reprise.

En mai, j’avais écrit un article sur les générateurs, un nouveau genre de fonction introduit avec ES6. J’en parlais comme d’une des fonctionnalités les plus magiques d’ES6 et je disais qu’ils pourraient faire partie intégrante de la programmation asynchrone. J’avais conclus cet article de la façon suivante :

Il y a encore à dire sur les générateurs.[…] Cependant, je pense que ça suffira pour ce billet, déjà suffisamment déconcertant. Comme le font les générateurs, il vaut mieux faire une pause et reprendre une autre fois.

C’est le moment. Vous pourrez trouver la première partie de cet article ici. Il est probablement préférable de la lire avant celui qui vient. Allez-y, c’est amusant, bon c’est un peu long et déroutant. Mais il y a des chatonmignons !

Un rapide coup d’œil dans le rétroviseur

La dernière fois, nous nous sommes concentrés sur le comportement de base des générateurs. C’est peut-être un peu bizarre, mais pas très diffcile à comprendre. Une fonction génératrice n’est pas comparable à une fonction ordinaire. La principale différence, c’est que le corps d’une fonction génératrice ne s’exécute pas d’un bloc en une seule fois. Il s’exécute, morceau par morceau, en prenant une pause à chaque fois qu’il atteint une expression yield.

L’article consacré à la première partie sur les générateurs expliquaient ces différentes notions mais nous n’avions pas déroulé d’exemple illustrant comment tout cela fonctionnait en détails. Allons-y.

function* desMots() {
  yield "coucou";
  yield "monde";
}

for (var mot of desMots()) {
  alert(mot);
}

Ce fragment de script est assez simple. Que se passerait-il en détails si vous pouviez observer les différents acteurs en jeu ? Ça donnerait un script assez différent qui ressemblerait à :

SCÈNE - Intérieur d’un ordinateur, le jour

BOUCLE FOR se tient seule sur scène avec un couvre-chef
et un écritoire, l’air affairé.

                          BOUCLE FOR
                          (appelant)
                           desMots()!
                              
GÉNÉRATEUR apparaît, grand, cuivré, avec une allure d’horloge.
Il a un air sympathique mais est aussi immobile qu’une statue.

                          BOUCLE FOR
               (frappant des mains gentiment)
              Très bien ! Maintenant, au travail.
                   (envers le générateur)
                            .next() !

GÉNÉRATEUR prend vie.

                         GÉNÉRATEUR
               {value: "coucou", done: false}

Il s’immobilise brusquement dans une pose maladroite.

                          BOUCLE FOR
                            alert !

ALERT surgit brusquement, l’air alerte et le souffle court. 
Il donne l’impression d’être toujours ainsi.

                          BOUCLE FOR
              Dites « coucou » à l’utilisateur.

ALERT se retourne et court hors de la scène.

                             ALERT
                  (hors de la scène, hurlant)
                          Arrêtez tout !
              La page web sur tech.mozfr.org dit :
                           "coucou"

Quelques secondes plus tard, après une pause, 
ALERT surgit à nouveau, traverse la scène vers 
BOUCLE FOR et dérape pour s’arrêter.

                            ALERT
                   L’utilisateur a dit OK.

                         BOUCLE FOR
              (frappant des mains gentiment)
                   Très bien ! Au travail.
              (se retournant vers GÉNÉRATEUR)
                           .next() !

GÉNÉRATEUR reprend vie.

                         GÉNÉRATEUR
               {value: "monde", done: false}

Il s’immobilise avec une autre pose, tout aussi gauche.

                          BOUCLE FOR
                            alert !

                           ALERT
                     (déjà à la course)
                      Je m’en occupe !
                 (hors de la scène, hurlant)
                         Arrêtez tout !
             La page web sur tech.mozfr.org dit :
                          "monde" !

S’en suit à nouveau une pause,
puis ALERT surgit sur scène, l’air découragé

                           ALERT
                L’utilisateur a dit OK mais...
                empêchez cette page d’ouvrir
                des dialogues supplémentaires.

Il sort, boudant.

                          BOUCLE FOR
                (frappant des mains gentiment)
                    Très bien ! au travail.
               (se retournant vers GÉNÉRATEUR)
                           .next() !

Pour la troisième fois, GÉNÉRATEUR reprend vie.

                         GÉNÉRATEUR
                        (l’air digne)
             {value: undefined, done: true}

Sa tête s’affaisse sur sa poitrine, ses yeux s’éteignent peu à peu.
Il ne bougera plus.

                          BOUCLE FOR
              C’est l’heure d’aller déjeuner.

Elle sort.

Après un moment, RAMASSE MIETTES entre, se saisit de GÉNÉRATEUR, sans vie, et l’emporte
en dehors de la scène.

Bon, ce n’est pas Hamlet, mais vous avez saisi l’idée.

Comme vous avez pu le constater pendant cette pièce, lorsqu’un objet générateur apparaît pour la première fois, il est interrompu. Il se réveille et travaille un peu, à chaque fois que la méthode .next() est appelée.

L’action est effectuée de façon synchrone et sur un seul thread. Vous noterez qu’à chaque instant, seul un des personnages est en train de jouer. Les personnages ne s’interrompent pas ni ne parlent en même temps que d’autres. Ils parlent chacun leur tour, pendant autant de temps que nécessaire (on dirait du Shakespeare !).

Une variation de ce drame se déroulera à chaque fois qu’un générateur sera utilisé dans une boucle for-of. Cette série d’appels à la méthode .next() n’apparaîtra pas n’importe où dans votre code. Pour cet exemple, je les ai mis sur scène mais pour vous et vos programmes, cela se déroulera en arrière-plan car les générateurs et les boucles for-of sont conçus pour fonctionner ensemble grâce à l’interface iterator.

Pour résumer ce qu’on a vu jusqu’à présent :

  • Les objets générateurs sont des robots cuivrés qui génèrent des valeurs
  • Pour programmer un robot, il suffit d’un morceau de code : le corps de la fonction génératrice qui l’a créé.

Comment arrêter un générateur

Les générateurs possèdent certaines fonctionnalités délicates que je n’ai pas abordées dans la première partie :

  • generator.return()
  • l’argument optionnel pour generator.next()
  • generator.throw(error)
  • yield*

Je m’étais arrêté sans elles car il est parfois difficile de comprendre pourquoi ces fonctionnalités existent. S’en souvenir précisément et savoir quand les utiliser semble un peu ardu à première vue. C’est assez difficile de s’en souvenir et de s’en rappeler. Cependant, au fur et à mesure qu’on utilise les générateurs et qu’on y réfléchit, on comprend leur utilité et on apprend à savoir quand les mettre en pratique.

Voici une façon de faire que vous avez probablement déjà utilisée :

function faireQuelqueChose() {
  initialisation();
  try {
    // ... faire quelque chose ...
  } finally {
    faireLeMénage();
  }
}

faireQuelqueChose();

La partie « ménage » s’applique à fermer des connexions ou des fichiers, à libérer des ressources système ou à mettre à jour le DOM pour interrompre une animation « en cours ». Ces étapes sont celles qu’on souhaite toujours avoir, y compris si les actions précédentes ont échoué. C’est pour cette raison qu’elles sont placées dans un bloc finally.

Que se passe-t-il si on applique cela à un générateur ?

function* produireDesValeurs() {
  initialisation();
  try {
    // ... générer des valeurs ...
  } finally {
    faireLeMénage();
  }
}

for (var valeur of produireDesValeurs()) {
  manipuler(valeur);
}

A priori ça a l’air bon. Cependant, il y a une subtilité qui s’est glissée ici : l’appel à manipuler(valeur) ne fait pas partie du bloc try. Si cet appel lève une exception, que se passera-t-il pour notre étape de ménage ?

Que se passe-t-il aussi quand la boucle for-of contient une instruction break ou return ?

Ce bloc de « ménage » s’exécutera tout de même. ES6 s’occupe de tout.

Lorsque nous avons abordé les itérateurs et la boucle for-of, nous avions vu que l’interface iterator contenait une méthode optionnelle .return() que le langage appelait automatiquement à la fin de l’itération, avant que l’itérateur déclare qu’il a fini. Les générateurs supportent cette méthode. Quand on appelle monGénérateur.return(), celui-ci exécute ses blocs finally s’il en a. Ensuite, il finit, de la même façon que si yield avait mystérieusement été transformé en une instruction return.

Vous noterez que la méthode .return() n’est pas appelée automatiquement dans tous les contextes. Elle est uniquement appelée pour les cas utilisant le protocole d’itération. Il est donc possible que le ramasse-miettes récupère le générateur sans que celui-ci n’ait exécuté son bloc finally.

À quoi ressemblerait cette fonctionnalité sur scène ? Le générateur est immobile, interrompu au milieu d’une tâche qui a besoin d’être préparée (par exemple la construction d’un gratte-ciel). Tout à coup, quelqu’un provoque une erreur ! La boucle for l’attrape et la met de côté. La boucle indique au générateur de lancer sa méthode .return(). Le générateur prend son temps, démantèle son échafaudage puis s’éteint. Une fois le générateur terminé, la boucle for reprend l’erreur et le mécanisme habituel de gestion des erreurs peut alors prendre le relai.

Au tour des générateurs

Jusqu’à présent, les échanges entre un générateur et l’utilisateur étaient assez unilatérales. Pour changer de l’analogie avec le théâtre, voici un nouvel exemple : img1.png C’est l’utilisateur qui dicte la conduite générale. Le générateur exécute le travail à la demande. Les générateurs peuvent être utilisés différemment.

Dans la première partie, j’ai dit que les générateurs pouvaient être utilisés pour programmer de façon asynchrone et qu’ils pourraient réaliser ce qu’on fait actuellement avec des fonctions de rappel (callback) asynchrone ou avec des chaînes de promesses. Vous vous êtes sans doute demandé comment cela pouvait fonctioner. Pourquoi yield est-il suffisant ? (après tout c’est le seul super pouvoir des générateurs) Le code asynchrone ne génère pas des valeurs, il réalise des actions. Il utilise des données de fichiers ou de base de données, il envoie des requêtes vers des serveurs. Enfin, il revient dans la boucle des événements pour attendre que ces processus asynchrones aient fini. Comment faire avec les générateurs ? Sans fonction de rappel, comment le générateur peut-il recevoir des données envoyées depuis des fichiers, des bases de données ou des serveurs ?

Pour avoir une idée de la réponse, imaginez ce qui se passerait si vous aviez un moyen de passer une valeur au générateur avec .next(). Grâce à ce seul changement, nous pourrions avoir un nouveau genre de conversation : img2.png

En fait, la méthode .next() d’un générateur peut prendre un argument. Encore mieux, l’argument utilisé apparaît dans le générateur comme la valeur renvoyée par l’expression yield. Cela signifie que yield n’est pas une instruction comme return; c’est une expression qui possède une valeur une fois que le générateur reprend.

var results = yield getDataAndLatte(request.areaCode);

Cette petite ligne fait beaucoup de choses :

  • Elle appelle getDataAndLatte(). Disons que cette fonction renvoie la chaîne “fournissez-moi les données pour la zone…” comme vu dans la capture d’écran.
  • Elle interrompt le générateur qui génère la chaîne de caractères.
  • Du temps passe…
  • Quelqu’un appelle ensuite .next({data: ..., café: ...}). On enregistre cet objet dans une variable locale et on poursuit avec la prochaine ligne de code.

Pour illustrer cet exemple avec un contexte, voici le code qui correspond à la conversation précédente :

function* handle(request) {
  var results = yield getDataAndLatte(request.areaCode);
  results.coffee.drink();
  var target = mostUrgentRecord(results.data);
  yield updateStatus(target.id);
}

Vous noterez que yield a toujours la même signification qu’avant : il met le générateur en pause et passe une valeur à l’appelant. Mais les choses ont changé ! Le générateur attend un certain support de la part de l’appelant. Le générateur s’attend à ce que l’appelant agisse comme un assistant.

Les fonctions ordinaires ne ressemblent généralement pas à ça. La plupart du temps, elles existent pour répondre aux besoins de l’appelant. En revanche, les générateurs peuvent être utilisés pour avoir des conversations et cela permet d’avoir de nouvelles relations entre les générateurs et leurs appelants.

À quoi pourrait ressembler cet assistant de générateur ? Il n’est pas nécessaire qu’il soit compliqué. Par exemple, il pourrait ressembler à :

function runGeneratorOnce(g, result) {
  var status = g.next(result);
  if (status.done) {
    return;  // pfiou !
  }

  // Le générateur nous a demandé d'aller chercher
  // quelque chose et de l'appeler à nouveau
  // quand ce serait fait
  doAsynchronousWorkIncludingEspressoMachineOperations(
    status.value,
    (error, nextResult) => runGeneratorOnce(g, nextResult));
}

Pour utiliser cet assistant, il suffit de créer un générateur et de l’exécuter une fois, de cette façon :

runGeneratorOnce(handle(request), undefined);

En mai, j’ai évoqué Q.async() comme un exemple de bibliothèque qui traite les générateurs comme des processus asynchrones et qui les lance automatiquement quand on en a besoin. runGeneratorOnce agit de cette façon. En pratique, les générateurs ne génèreront pas des chaînes de caractères pour indiquer à l’appelant ce qu’il faut faire, ils utiliseront probablement des promesses (objets Promise).

Si vous comprenez comment utiliser les promesses et que vous avez compris comment utiliser les générateurs, vous pouvez essayer de modifier runGeneratorOnce pour qu’il supporte les promesses. Cet exercice n’est pas un exercice facile mais une fois que vous en serez venu à bout, vous serez capable d’écrire des algorithmes asynchrones complexes qui utilisent des promesses et qui sont écrits en quelques lignes. La cerise sur le gateau : aucun .then() ou aucune fonction de rappel (callback) en vue !

Comment faire exploser un générateur

Avez-vous remarqué comment runGeneratorOnce gère les erreurs ? Il les ignore complètement !

C’est problématique, nous préfèrerions qu’il existe un moyen de rapporter l’erreur au générateur. Les générateurs permettent de le faire, vous pouvez appeler generator.throw(erreur) plutôt que generator.next(result). Cela permet de lever une exception avec l’expression yield. Comme pour .return(), le générateur s’éteindra mais si le yield utilisé est contenu dans un bloc try, les instructions des blocs catch et finally seront utilisées afin que le générateur puisse se terminer de façon correcte.

Vous pouvez également modifier runGeneratorOnce pour vous assurer que .throw() est appelé quand c’est nécessaire, c’est un autre exercice intéressant. Attention, gardez à l’esprit que les exceptions levées à l’intérieur de générateurs sont toujours propagées vers l’appelant. generator.throw(erreur) lèvera une erreur directement dans le code appelant sauf si le générateur l’attrape pour la gérer !

Cela complète la liste des cas de figures possibles qui se produisent quand un générateur atteint une expression yield et s’interrompt :

  • Quelqu’un peut appeler generator.next(valeur). Dans ce cas, le générateur reprend son exécution là où il s’était arrêté.
  • Quelqu’un peut appeler generator.return(), éventuellement avec une valeur. Dans ce cas, le générateur ne continue pas avec ce qu’il était en train de faire, il exécute uniquement le bloc finally.
  • Quelqu’un peut appeler generator.throw(erreur). Dans ce cas le générateur se comporte comme si l’expression yield était un appel à une fonction qui a levé une exception.
  • Ou alors, personne ne fait rien. Dans ce cas le générateur reste gelé à jamais (oui, il est possible qu’un générateur entre dans un bloc try et n’exécute jamais le bloc finally). Le ramasse-miettes peut récupérer le générateur dans cette situation.

Il n’y a pas grande différence avec un appel de fonction classique. Seul .return() est réellement une nouvelle possibilité.

En fait, yield possède beaucoup de points communs avec les appels de fonctions. Lorsque vous appelez une fonction, vous êtes temporairement en attente n’est-ce pas ? C’est la fonction que vous appelez qui est aux commandes, elle peut renvoyer un résultat, lever un exception ou encore boucler infiniment.

Combiner des générateurs

Je voudrais vous montrer une dernière fonctionnalité. Imaginons qu’on écrive une fonction génératrice qui concatène deux objets itérables :

function* concat(iter1, iter2) {
  for (var value of iter1) {
    yield value;
  }
  for (var value of iter2) {
    yield value;
  }
}

ES6 propose un raccourci pour ceci :

function* concat(iter1, iter2) {
  yield* iter1;
  yield* iter2;
}

Un expression yield « simple » génère une seule valeur. Une expression yield* consomme un itérateur entier et génère toutes ses valeurs.

Cette même syntaxe permet de résoudre un autre problème : comment appeler un générateur depuis un générateur ? Avec les fonctions classiques, on peut isoler un morceau de code d’une fonction pour le placer dans une fonction séparée et le réutiliser sans changer le comportement. Bien évidemment, on voudra aussi refactoriser des générateurs. Or, pour cela, il nous faut une méthode pour appeler le fragment refactorisé et s’assurer que chaque valeur qui était générée avant est toujours générée avec ce nouveau sous-programme. yield* permet de faire cela :

function* générateurExtrait() { ... }

function* fonctionRefactorée() {
  ...
  yield* générateurExtrait();
  ...
}

Vous pouvez voir cela comme une tâche qu’un robot délègue à un autre. Vous pouvez voir ici combien cette idée est importante pour écrire de grands projets utilisant des générateurs et pour garder le code propre et organisé. C’est aussi important que les fonctions pour organiser du code synchrone.

Exeunt

Bien, c’est terminé pour les générateurs ! J’espère que vous avez apprécié cette découverte autant que moi !

La semaine prochaine, nous parlerons d’une fonctionnalité ES6 entièrement nouvelle et surprenante. Il s’agit d’un nouveau genre d’objet, subtil et retors : vous pourrez en utiliser un sans même le savoir. Rendez-vous donc la semaine prochaine pour étudier les proxies ES6 en détails.