Javascript les générateurs (generators)

59
Javascript Les générateurs

Transcript of Javascript les générateurs (generators)

Javascript

Les générateurs

Préambule ...

Nous allons parler ici de fonctionnalités qui ne sont disponibles que depuis ECMAScript 6.

Leurs support n’est pas généralisé ; par exemple pas de IE, ou bien pas de Node.js (sauf si activation de --harmony )

http://kangax.github.io/compat-table/es6/#generators

https://github.com/google/traceur-compiler

Itérateurs ?

En programmation, nous utilisons des boucles.

Itérateurs ?

for (var i = 0; i < 10; i++) { // Faire quelque chose 10 fois.}

while(condition) { // Faire quelque chose jusqu'à ce que ...}

Le principe est de répéter l'exécution d’un code un certain nombre de fois.

Chaque “tour” de notre boucle est une itération.

Parcourir cette boucle se dit donc itérer.

Itérateurs ?

Il est aussi possible d’itérer sur un tableau :

Itérateurs ?

var valeurs = [ 'a', 'b', 'c' ];

// Itérer sur les indices du tableau :for (var indice in valeurs) { console.log(valeurs[indice ]);}

// Ou utiliser la méthode Array.forEach()valeurs.forEach(function(valeur) { console.log(valeur);});

Ou un objet :

Itérateurs ?

var valeurs = {a : 1, b : 2, c : 3 };

// Itérer sur les propriétés :for (var indice in valeurs) { if (valeurs.hasOwnProperty(indice)) { console.log( '[%s] => %s', indice, valeurs[indice] ); }}

On considère alors que les tableaux et les objets sont des itérateurs.

Mais comme on vient de le voir, on itère sur les indices ou propriétés.

C’est pas très pratique.

Itérateurs ?

ECMAScript 6 (es6) introduit une nouvelle syntaxe d’itération : for ( … of …) :

Itérateurs ?

var valeurs = [3, 4, 5] ;

for (var indice in valeurs) { console.log(indice);}// 0 ... 1 ... 2

for (var valeur of valeurs) { console.log(valeur);}// 3 ... 4 ... 5

\o/ joie, bonheur, alacrité nous pouvons donc désormais itérer sur des tableaux et objets sans recoder la roue ou utiliser des Frameworks (jQuery $.each, par exemple).

Itérateurs ?

Seulement, dehors il y a le vrai monde...

Itérateurs ?

Et le vrai monde, il veut itérer, et si possible de manière optimisée et pas que sur des tableaux et des objets.

Petit interlude philosophie, optionnel.

Cliquez pour visionner.

Pourquoi c’est nul ?

L’algorithme consistant à remplir un tableau/objet puis d’itérer dessus a beau avoir fait la route avec nous depuis nos premières pages, il présente de sérieux défauts.

Dénigrons !

1.Il veut même pas rendre ses ressources. Radin.

Dénigrons !

Le tableau/objet est généré puis stocké en mémoire entièrement pendant toute l’itération de notre boucle, même si nous n’avons plus besoin de ces valeurs.

Dénigrons !

// Création de ma liste.var liste = [];

// Remplissagefor (var i = 0; i < 1000000; i++) { liste.push(Math.random());}

// Affichagefor (var val of liste) { console.log(val);}

Si l’on considère le code suivant :

L’importante partie de mémoire utilisée pour stocker les 1 million de valeurs va rester allouée pendant toute l’itération de la boucle ; même si nous n’avons plus besoin de ces valeurs après le console.log().

2.Il a la manie de tout bloquer.

Dénigrons !

Pendant toute la création puis l’itération du tableau/objet, le navigateur ou le processus est “occupé”. Cela peut retarder la gestion d’évènements, ralentir des affichages et rendre instable le navigateur/programme.

Dénigrons !

// Création de ma liste.var liste = [];

// Remplissageconsole.time('Remplissage du tableau');for (var i = 0; i < 1000000; i++) { liste.push(Math.random());}console.timeEnd('Remplissage du tableau');

// Affichageconsole.time('Affichage du tableau');for (var val in liste) { console.log(val);}console.timeEnd('Affichage du tableau');

// Remplissage du tableau: 2054.109ms// Affichage du tableau: 2185.662ms

Ajoutons un timer à notre code :

Ici, on voit que l’on a été “bloqué” pendant deux fois 2 secondes.

Faire mumuse sur JSFiddle

Dénigrons !

// Remplissageconsole.time('Remplissage du tableau');for (var i = 0; i < 1000000; i++) { liste.push(Math.random());}console.timeEnd('Remplissage du tableau');

// Affichageconsole.time('Affichage du tableau');for (var val of liste) { console.log(val);}console.timeEnd('Affichage du tableau');

// Remplissage du tableau: 2067.762ms// Affichage du tableau: 238779.176ms

A noter également que for(... of …) devient même problématique …

On a quasiment 10x le temps d’affichage (On fait un reparcours du tableau à chaque itération vers la fin).

Dénigrons !

A tester vous-même :

http://jsfiddle.net/jucrouzet/fhgkaLgt/

3.Il n’est vraiment pas souple...

Dénigrons !

Une seul choix nous est offert : du début à la fin du tableau / objet, si l’on souhaite itérer différemment, on est obligés de “bricoler”.

Dénigrons !

var liste = [ 1, 2, 3, ....];

// Incrémenter de deux en deux ?for (var i in liste) { if (i % 2) { // Mon code a éxecuter à 1 3 4 ... } // Autant écrire // } else { ignorer cette mémoire gaspillée }}// Sinon, il faut faire la même chose lors de la génération

Toute coquetterie est interdite, on se limite à :

Wow. Folie.

Sinon le code devient vite illisible.

Et donc, les générateurs !

Le générateur est un concept assez répandu dans beaucoup d’autres langage.

Générateurs

Mainstreamlabel !

Imaginez qu’au lieu d’itérer sur une liste déjà

prête, vous allez générer des valeurs au fur et à mesure (et en parallèle) de votre itération ...

Générateurs

Générateurs

Processus d’itération classique :

Génération d’une liste

Stockage de la liste

Itération dans la liste

Liste finie ?

Non

Oui

Générateurs

Processus d’itération classique :

Génération d’une liste

Stockage de la liste

Itération dans la liste

Liste finie ?

Non

Oui

Opération

synchrone/bloquante

Opération

synchrone/bloquante

Générateurs

Processus générateur :

Création d’un générateur

Stockage du générateur en

mémoire

Attente / Récup prochain élément

dans la pile du générateur

Générateur Fini ?

Non

Oui

Génération d’un élément de

la liste

Empiler l’élément

Fini ?

Non

Oui

En parallèle

Générateurs

Pour comprendre le “en parallèle”, voir“Event Loop” dans :

http://slideshare.net/jucrouzet/promises-javascript

Générateurs

On va donc itérer non plus sur une liste

prédéfinie mais sur des valeurs que l’on peut générer de la manière que l’on veut.

Générateurs

Le système de pile de données entre l’itération et la génération gérera la synchronisation lorsque l’un génère ou consomme plus vite que l’autre.

Alors c’est parti !

Générateurs ECMAScript 6

Les générateurs en es6 sont définis grâce à l’ajout d’une étoile (*) sur le mot-clé function

function* genereValues(arg1, arg2) { //Genere mes valeurs.}

// ou

var genereValues = function* (quelquechose) { //Genere mes valeurs.};

Générateurs ECMAScript 6

Un appel à cette “fonction” va donc retourner un objet qui est un itérateur :

for (var valeur of genereValues(42, 'chihuahua')) {

// Faire quelque chose

}

Notez bien : for (... of …) et non for (... in …) !

Générateurs ECMAScript 6

Occupons-nous maintenant du corps de ce générateur afin qu’il génère effectivement quelque chose...

Générateurs ECMAScript 6

Le corps d’un générateur est identique à une fonction normale

var generateur = function* (value) { // Corps du générateur var uneVariable = 42;

while(true) { uneVariable += value; } // Fin corps du générateur};

Générateurs ECMAScript 6

A la différence près qu’il ne retourne pas de valeur mais qu’il en génère.On utilise donc ici le le mot-clé yield (genère)

var generateur = function* (value) { var uneVariable = 42;

while(true) { uneVariable += value; yield uneVariable; }};

Contrairement à return, yield ne vas pas interrompre l’exécution de la fonction.

Générateurs ECMAScript 6

On a donc :

var generateur = function* (value) { var uneVariable = 42;

while(true) { uneVariable += value; yield uneVariable; }};

for (var valeur of generateur(10)) { console.log(valeur); // 52 … 62 … 72 … 82 … (vers l’infini et au delà)}

Générateurs ECMAScript 6

Comme vous pouvez le lire, l’itération va tourner en infini.Ce n’est probablement pas ce que l’on veut.

var generateur = function* (value) { var uneVariable = 42;

while(value >= 0) { uneVariable += value; yield uneVariable; value--; }};

for (var valeur of generateur(5)) { console.log(valeur); // 47 … 51 … 54 … 56 … 57}

Faire mumuse sur JSFiddle

Générateurs ECMAScript 6

Maintenant que nous avons fait notre premier générateur (qui n’a rien de mieux qu’une itération) ; allons un peu plus loin dans l’inspection de l’objet Generator ...

Générateurs ECMAScript 6

Inspectons le retour de generateur() dans l’inspecteur d’une console JS :

var iterateur = generateur(10);console.log(iterator);>> Generator { }// Nous avons donc bien objet de type Generator,// ouvrons le avec l’inspecteur en utilisant les// variables de substitution de console.log() :console.log('%o', iterator);

Générateurs ECMAScript 6

Nous savons donc que Generator est une classe qui offre deux méthodes publiques : next() et throw()

Générateurs ECMAScript 6

Comme son nom l’indique, next() est un appel à dépiler la prochaine valeur générées par notre générateur.Si elle est disponible, elle est retournée, sinon, on considère la boucle finie.

Analysons le retour de Generator.next() ...

Générateurs ECMAScript 6

var generateurVide = function* () {};var iterateurVide = generateurVide();

console.log(iterateurVide.next());

// >> Object { value: undefined, done: true }

var generateur = function* () { yield 'AC/DC'; yield 'Iron Maiden'; yield 'Justin Bieber'; yield 'Metallica';};var iterateur = generateur();

console.log(iterateur.next());console.log(iterateur.next());console.log(iterateur.next());console.log(iterateur.next());console.log(iterateur.next());

// >> Object { value: "AC/DC", done: false }// >> Object { value: "Iron Maiden", done: false }// >> Object { value: "Justin Bieber", done: false }// >> Object { value: "Metallica", done: false }// >> Object { value: undefined, done: true }

Faire mumuse sur JSFiddle

Générateurs ECMAScript 6

Le fonctionnement des Generator dans une boucle for (... of … ) est donc maintenant simple à comprendre :

Chaque itération fait un appel à .next(), si l’objet de retour a la propriété done à false, elles assignent la valeur de la propriété value pour cette itération.

Si l’objet de retour a la propriété done à true, elles s’arrêtent immédiatement.

Générateurs ECMAScript 6

Afin maintenant de comprendre comment la synchronisation est faite, passons à un autre test.Un test champêtre.

Générateurs ECMAScript 6

var generateurPoete = function* () { console.log('Un Kalachnikov dans le boule en introduction'); yield 'b'; console.log('Ouest Side, Ouest Side, 92 injection'); yield '2o'; console.log('Certains croivent qu\'ils rivalisent, faudra qu\'on les hospitalise'); yield 'b'; console.log('Tu tiens pas la route, pé-ra avec un "A" collé dans le dos'); yield 'a';};

var neuf2_izi = generateurPoete();

console.log(neuf2_izi.next());console.log(neuf2_izi.next());console.log(neuf2_izi.next());console.log(neuf2_izi.next());console.log(neuf2_izi.next());

Faire mumuse sur JSFiddle

Donc le code suivant :

Générateurs ECMAScript 6

"Un Kalachnikov dans le boule en introduction" Object { value: "b", done: false }

"Ouest Side, Ouest Side, 92 injection"Object { value: "2o", done: false }

"Certains croient qu'ils rivalisent, faudra qu'on les hospitalise" Object { value: "b", done: false }

"Tu tiens pas la route, pé-ra avec un "A" collé dans le dos" Object { value: "a", done: false }

Object { value: undefined, done: true }

Affichera dans la console :

Générateurs ECMAScript 6

Comme vient de nous le prouver Booba, l’instruction yield empile une valeur puis mets en pause l’exécution du générateur jusqu'à ce que .next() soit appelé pour la dépiler.

Générateurs ECMAScript 6

Cette technique présente deux avantages :

- La mémoire n’est pas allouée pour des tonnes de valeurs mais uniquement pour la valeur en cours d’itération ;

- Les valeurs ne sont pas générées dans une boucle qui utilise toute les ressources le temps de la génération mais uniquement “à la demande”.

Générateurs ECMAScript 6

Un autre avantage des générateurs est que la “communication” entre le Generator et son consommateur (l'appelant de .next()) n’est pas unidirectionnelle, c’est un dialogue !

Générateurs ECMAScript 6

La méthode .next() accèpte un argument, c’est la valeur qui sera retournée par yield.

Donc si :

monGenerator.next('coucou');

alors dans le générateur :

var message = yield 'valeur générée';

// message est égale à ‘coucou’

Générateurs ECMAScript 6

Appliquons ça sur un exemple dramatique :

Générateurs ECMAScript 6

var patronat = function* () { var tours = 3; while(tours) { var demande = yield 'rien';

window.console.log( 'Patron : Parce que la dernière fois vous avez déjà demandé %s', demande ); tours--; } };

var dialogueSocial = patronat();

var syndicat = function(demande) { var reponse = dialogueSocial.next(demande); if (reponse && !reponse.done && reponse.value) { window.console.log('Syndicat : On a demandé %s et on a eu => %s', demande, reponse.value); } else { window.console.log('Syndicat : On démarre une grève'); }};

syndicat('une augmentation');syndicat('des congés');syndicat('des tickets resto');syndicat('des RTT');

Faire mumuse sur JSFiddle

Générateurs ECMAScript 6

"Syndicat : On a demandé une augmentation et on a eu => rien"

"Patron : Parce que la dernière fois vous avez déjà demandé des

congés"

"Syndicat : On a demandé des congés et on a eu => rien"

"Patron : Parce que la dernière fois vous avez déjà demandé des

tickets resto"

"Syndicat : On a demandé des tickets resto et on a eu => rien"

"Patron : Parce que la dernière fois vous avez déjà demandé des

RTT"

"Syndicat : On démarre une grève"

ECMAScript 6, c’est un coup monté du patronat.

Je vois que ça.

Générateurs ECMAScript 6

Bon, ce dialogue a bien dérapé.Les bugs ça arrive.

Tiens d’ailleurs on a pas parlé de l’autre méthode de Generator, .throw().

Magnifique transition.

Générateurs ECMAScript 6

La méthode .throw() permet, toujours dans cet esprit de dialogue constructif, au consommateur du générateur (celui qui appèle .next(), par exemple) de lever une exception dans le corps de la fonction du Generator, au niveau du yield.

Disons le sans détour, c’est un moyen de torpiller de l’intérieur !

Heureusement, un gilet pare-balles nommé try/catch est là pour gérer ça.

Générateurs ECMAScript 6

.throw() prend donc un argument, typiquement une chaîne de caractère (message d’erreur), ou un objet Error.

Cette valeur sera lancée comme Exception lors du prochain appel de yield.

Générateurs ECMAScript 6

function* monGenerateurQuonEmbete() { var loops = 5; while(loops) { try { yield loops; } catch(err) { console.log('Aie, je viens de recevoir %o', err); // 1 -> return; } loops--; } };

var victime = monGenerateurQuonEmbete();

console.log(victime.next()); // 2 ->console.log(victime.next()); // 3 ->console.log(victime.throw(new Error('Et pan dans les dents'))); // 4 ->console.log(victime.next()); // 5 ->

>> Object { value: 5, done: false } // <- 2>> Object { value: 4, done: false } // <- 3>> "Aie, je viens de recevoir Error: Et pan dans les dents [...]" // <- 1>> Object { value: undefined, done: true } // <- 4>> Object { value: undefined, done: true } // <- 5

Faire mumuse sur JSFiddle

Générateurs ECMAScript 6

Fin du premier chapitre.

Dans le prochain, on explorera comment coupler les générateurs et les promesses.

Tout un programme.

Mais ça vaut vraiment le coup.

Promis.

Pour me contacter :