ES2015 – Itérateurs et générateurs – Itérables et itérateurs



ES2015 – Itérateurs et générateurs – Itérables et itérateurs

0 0


nodejsparis-es2015-iterateurs-generateurs

(In French) Presentation at meetup NodeJS Paris on 2016-01-19 about iterators and generators in ES2015

On Github fmauquie / nodejsparis-es2015-iterateurs-generateurs

ES2015

Itérateurs et générateurs

Node.js Paris ∾ 19/01/2016

Fabien Mauquié

Github

  • Itérables et itérateurs
  • Générateurs
  • Cas d'utilisation

ES2015

  • Le Javascript d'aujourd'hui (17 juin 2015)
  • node
  • Chrome, Firefox, Edge
  • Transpilers

Itérables et itérateurs

  • Protocole commun pour l'itération
  • Implémenté par les collections (Set, Map, …)
  • Utilisé par plein d'opérateurs

for ... of

for (let [i, elem] of anArray.entries()) {
    console.log(`index: ${i}, value: ${elem}`);
}
                        

Spread

let aMap = new Map().set('a', 1).set('b', 2);
aMap.entries(); // MapIterator { [ 'a', 1 ], [ 'b', 2 ] }

let plop = ['p', ...aMap.entries(), 'lop'];
plop; // [ 'p', [ 'a', 1 ], [ 'b', 2 ], 'lop' ]
                        

Déstructuration

let aSet = new Set().add('a').add('b');

let [a, b] = aSet;
a; // 'a'
b; // 'b'

aSet.add('c');
let [a2, ...bc] = aSet;
bc; // [ "b", "c" ]
                        
Pas encore implémenté dans V8

Mais aussi…

  • Array.from()
  • Promise.all(iterableOverPromises).then(…); Promise.race(iterableOverPromises).then(…);
  • yield*

Object

let obj = {prop1: true, prop2: false};

for (let prop of obj) {
// TypeError: obj[Symbol.iterator] is not a function
}

Implémenter un itérable

let iterable = {
    [Symbol.iterator]() {
        let step = 0;
        return {
            next() {
                if (step <= 2) {
                    step++;
                }
                switch (step) {
                case 1:
                    return { value: 'Une valeur', done: false };
                case 2:
                    return { value: 'deux valeurs', done: false };
                default:
                    return { value: undefined, done: true };
                }
            }
        };
    }
};

AAAAAAAAAAAAAAAH !

Les protocoles d'itération

  • Symbol.iterator
  • next()
  • {value: 'plop1', done: false}
  • {value: 'plopFinal', done: true}

Définition

let iterable = {
    [Symbol.iterator]() {
        return {
            value: 0,
            next() {
                return {value: ++value};
            }
        };
    }
};

Itérateurs itérables

let iterable = {
    [Symbol.iterator]() {
        return this;
    },
    currentValue: 0,
    next() {
        return {value: ++currentValue};
    }
};

C'est comme ça dans les collections ES2015

let arr = [];
let iterator = arr[Symbol.iterator]();
iterator[Symbol.iterator]() === iterator; // true
let peopleCSV = [['Nom', 'Prénom'], ['Fabien', 'Mauquié']];
let itPeople = peopleCSV[Symbol.iterator]();

// Remove header line
itPeople.next();

for (let [name, surname] of itPeople) {
    console.log(`Name: ${name}, Surname: ${surname}`);
}
let arr = ['a', 'b'];
let iterator = arr[Symbol.iterator]();

iterator.next(); // {value: 'a', done: false}
iterator.next(); // {value: 'b', done: false}
iterator.next(); // {value: undefined, done: true}

Générateurs

  • Fonctions qui peuvent se mettre en pause
    • function*
    • yield
  • Implémenter des itérables (facilement)
  • Linéariser du code asynchrone

Un exemple !

function* gen() {
    console.log('A');
    yield 'A';
    console.log('B');
    return 'B';
}

let iterator = gen();

iterator.next(); // console: 'A'; {value: 'A', done: false}
iterator.next(); // console: 'B'; {value: 'B', done: true}
iterator.next(); // {value: undefined, done: true}

Création

// Déclaration
function* gen() {
    // ...
}

// Assignation
let gen2 = function* () { /* ... */ };

// Dans un objet
let obj = {
    * gen() { // gen: function* gen() {
        // ···
    }
};

// Classe. Aucun navigateur ne supporte class aujourd'hui
// FF et Edge s'y mettent...
class UneClass {
    * gen() {
        // ···
    }
}

Rôles des générateurs

  • Itérateurs (producteurs de données)
  • Observateurs (consommateurs de données)
  • Coroutines (les deux)

Créer des itérateurs avec les générateurs

yield et return

function* gen() {
    yield 'A';
    return 'B';
}

let iterator = gen();

iterator.next(); // {value: 'A', done: false}
iterator.next(); // {value: 'B', done: true}
for (let valeur of gen()) {
    console.log(valeur);
}

[...gen()]; // ['A']

La plupart des structures d'itération ne prennent pas en compte la valeur de retour

yield*

function* gen2() {
    yield 'C';
    let b = yield* gen();
    yield b;
}

[...gen2()]; // [ 'C', 'A', 'B' ]
function* gen3() {
    yield* ['un', 'autre', 'iterable'];
}

[...gen3()]; // [ 'un', 'autre', 'iterable' ]

Itéreration récursive

function* iterateOnTree(head) {
    if (!head) return;

    yield head.value;
    yield* iterateOnTree(head.left);
    yield* iterateOnTree(head.right);
}

[...iterateOnTree({
    value: 'A',
    left: {
        value: 'B',
        left: {value: 'C'}
    },
    right: {
        value: 'D'
    }
})]; // [ 'A', 'B', 'C', 'D' ]

yield ne fonctionne que dans un générateur

function* aie() {
    setTimeout(() => (yield));
}
// SyntaxError: arrow function may not contain yield

Générateurs consommateurs

  • Envoyer des valeurs avec next(value)
  • Forcer un retour avec return(value)
  • Lancer une erreur avec throw(error)

Écouter des valeurs

function* consommateur() {
    console.log('Démarré');
    console.log(`1. ${yield}`);
    console.log(`2. ${yield}`);
    return 'termine';
}

let iterator = consommateur();
// "pomper" pour démarrer
iterator.next(); // Démarré ; {value: undefined, done: false}

// Envoyer des valeurs
iterator.next('un !');
// 1. un !   ; {value: undefined, done: false}
iterator.next('deux !');
// 2. deux ! ; {value: 'termine', done: true}

yield est asymétrique

  • yield renvoie la valeur à sa droite
  • L'exécution s'arrête au yield
  • L'exécution reprend à gauche de yield avec la valeur passée à next()

return()

Force un 'return' à l'endroit où le générateur est arrêté

function* avecReturn() {
    try {
        console.log('Démarré');
        yield;
        console.log('On ne devrait pas passer là');
    } finally {
        console.log('on est parti');
    }
}

let iterator = avecReturn();
iterator.next(); // Démarré ;  {value: undefined, done: false}

// Forcer un retour
iterator.return('stop');
// on est parti ; {value: 'stop', done: true}

Capturer le return()

function* empecherReturn() {
    try {
        console.log('Démarré');
        yield;
        console.log('On ne devrait pas passer là');
    } finally {
        yield 'ahah!';
    }
}

let iterator = empecherReturn();
iterator.next(); // Démarré ;  {value: undefined, done: false}

iterator.return('stop'); // {value: 'ahah!', done: false}
iterator.next();         // {value: 'stop', done: true}
Documentez !

throw()

Lance une erreur à l'endroit où le générateur est arrêté

function* avecThrow() {
    console.log('Démarré');
    yield;
    console.log('On ne devrait pas passer là');
}

let iterator = avecThrow();

// Forcer une erreur
iterator.throw(new Error('aaaaargh !'));
// Error: aaaargh !

Il est possible de catcher l'erreur avec try { ... } catch(e) { ... }

Générateurs nouveaux-nés

function* newborn() {
    yield;
}

let iterator = newborn();

iterator.next('une valeur');
// Erreur ! on n'est pas sur un yield

iterator.return('plop');                 // OK
iterator.throw(new Error('aaaaargh !')); // OK

Et avec yield* ?

  • throw() se comporte comme une exception, elle se propage du yield où on est en pause vers l'appelant
  • return() se comporte aussi comme une exception (par rapport aux finally)

Pattern lazy pushing

function* sum(sink) {
    var accumulator = 0;
    try {
        while(true) {
            accumulator += yield;
        }
    } finally {
        sink(accumulator);
    }
}

function* filterNumbers(sink) {
    sink.next(); // Amorçage
    try {
        while (true) {
            let word = yield;
            if (/[\d+]/.test(word)) {
                sink.next(parseInt(word));
            }
        }
    } finally {
        sink.return();
    }
}

let iterator = filterNumbers(sum(console.log.bind(console)));
iterator.next(); // Amorçage

iterator.next('J\'ai');
iterator.next('20');
iterator.next('minutes');
iterator.next('pour');
iterator.next('120');
iterator.next('slides');
iterator.return();

Coroutines

  • yielder des promises
  • next() avec le résultat
  • throw() avec l'erreur

Rappel: les promises

fetch('json/entry.json')
    .then(function(response) {
        return response.json();
    })
    .then(function(json) {
        return json.id;
    })
    .then(function(id) {
        return fetch('/json/' + id);
    })
    .then(function(response) {
        return response.json();
    })
    .then(function(specifics) {
        return Promise.all(specifics.map(obj => fetch('/json/' + obj.id));
    })
    .then(function(fetchedObjects) {
        return Promise.all(fetchedObjects.map(obj => obj.json()));
    })
    .then(console.log.bind(console, 'objets:'))
    .catch(console.error.bind(console, 'aaargh'))
;

... en coroutine

function* fetchObjectList() {
    let response = yield fetch('json/entry.json');
    let id = (yield response.json()).id;

    let specifics = yield fetch(`json/${id}`);

    var fetchedObjects = yield Promise.all(
        (yield specifics.json()).map(obj => fetch(`json/${obj.id}`))
    );

    return yield Promise.all(fetchedObjects.map(obj => obj.json()));
}

co(function* () {
    try {
        let objects = yield* fetchObjectList();

        console.log('objets:', ...objects);
        return objects;
    } catch(e) {
        console.error('aaargh', e);
    }
});

Lancer les coroutines

Si on sait qu'on ne yield que des promises…

function co(generator) {
    let iterator = generator();

    let runNext = function(promise) {
        promise.then(function(res) {
            let next = iterator.next(res);
            if (!next.done) {
                runNext(next.value);
            }
        })
        .catch(function(err) {
            iterator.throw(err);
        });
    }

    runNext(iterator.next().value);
}

return yield ?

function* returnYield() {
    return yield 'A';
}

let iterator = returnYield();

iterator.next();         // {value: 'A', done: false}
iterator.next('coucou!); // {value: 'coucou!', done: true}

Utilisation

  • Itérateurs
  • Mise en pause de tâches longues
  • Asynchrone (co)
  • Communicating Sequential Processes (js-csp)
  • Middlewares (Koa.js)

Pour aller plus loin

Merci !

Questions ?
ES2015 Itérateurs et générateurs Node.js Paris ∾ 19/01/2016 Fabien Mauquié Github