dresdenjs-generators



dresdenjs-generators

0 1


dresdenjs-generators


On Github hoeck / dresdenjs-generators

ES6 generators

Erik Soehnel
github.com/hoeck

what am I talking about?

function* gen () {
    for (let i=0; i<2; i++) {
        yield i;
    }
}

const g = gen();

g.next() // => { value: 0, done: false }
g.next() // => { value: 1, done: false }
g.next() // => { value: undefined, done: true }
// done
                    

this is a generator function

and this is how we use them

by the end of this talk, you'll know how to use *`yield`*

code convention

1 + 2 // => 3

_.range(3)
// => [
//   0
//   1
//   2
// ]

comment and fat arrow show the results of an expression works for multiline results too

Agenda

sequences in ES5 generators in ES6 wrap up

sequences in ES5 ?

we'll use one example

  • a sequence of unique IDs
  • e.g. as unique CSS classnames in generated styles
  • ['id1', 'id2', 'id3, ... 'idN']

ES5: Arrays all the way down

// in for loops
var ids = [];
for(i=0; i<3; i++) {
    ids.push('id'+i);
}

// with (almost base) libraries
_.range(3).map(function (x) { return 'id'+x; });
// => ['id0','id1','id2']

with plain for loops with libraries such as underscore

ES5: Or custom protocols

var i=0;
function nextNumber () {
    i += 1;
    return 'id'+i;
}

nextNumber() // => 'id1'
nextNumber() // => 'id2'

custom way to get the next element custom return type

2. generators in ES6

ES6: a proper iteration protocol (finally)!

interface Iterator {
    next(value) : IteratorResult;
    [optional] throw(value) : IteratorResult;
    [optional] return(value) : IteratorResult;
}

interface IteratorResult {
    value : any;
    done : bool;
}

next, steps through the values value is wrapped so we can distinguish between undefined and done let's use it

ES6: a proper iteration protocol (finally)!

ids = getIdIterator();

ids.next()
// => {
//    value: 'id1',
//    done: false
// }

ids.next() // => {value: 'id2', done: false}

setting the iterator up stepping through it ... ... returns an object value is the result of one iteration step done will be true if there are no more elements to iterate through

Support throughout the language

const chars = 'ab';

const it = chars[Symbol.iterator]();

it.next() // => {value: "a", done: false}
it.next() // => {value: "b", done: false}
it.next() // => {value: undefined, done: true}
it.next() // => ???

for example, Strings provide an iterator now the iterator is accessed via Symbol.iterator to avoid collisions iteration is always the same BTW, calling the generator after done: true results in {value: undefined, done: true}

You can construct maps from them, Arrays etc.

A good reference, as always is the MDN:

https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Iteration_protocols

And we can loop through them with for-of

sum = 0;
for(let x of [1,2,3]) {
    sum += x;
}

sum // => 6

for(let <item> of <iterable>)

Implementing a generator manually

class IdGenerator {
    constructor () {
        this._counter = 0;

        this[Symbol.iterator] = () => { return {next: this.next.bind(this) }};
    }

    next () {
        this._counter += 1;

        return {value: 'id' + this._counter, done: false};
    }
}

the generators state the iteration protocol the generator interface

Using our generator with for-of

gen = new IdGenerator();

res = [];
for (let x of gen) {
    res.push(x);

    if (res.length > 3) {
        break;
    }
}

res // => ['id1', 'id2', 'id3']

stop after taking 3 elements for-of gets the value for us

That's basically it.

  • generators are a design pattern
  • they are useful sometimes

but

To help writing generators,

ES6 introduced syntax sugar in form of

generator functions

generator function

function * idGenerator {
    let counter=0;

    while (true) {
        counter++;

        yield 'id' + counter;
    }
}

an asterisk * after function declares a generator function yield defines the value to return next

how does execution look like?

function* idGenerator () {
    console.log('start');
    yield 'id0';

    let counter=0;
    while (true) {
        console.log('loop');
        counter++;
        yield 'id' + counter;
    }
}

const g = idGenerator(); returns a generator object g.next(); executes all code up to the first yield ... execution of the generator is suspended and we get {value: 'id0', done:false} g.next(); enters the while-loop up to the second yield ... ... then it suspends and we get{value: 'id1', done:false}

as there is no stop condition in the loop g.next() will return a new id everytime its called

Q: How does that work behind the scenes?

A: It is compiled down to a state machine: Check the Babel REPL

pass control to another generator

function* genA () {
    yield 'a';
    yield 'b';
}

function* wrap (g) {
    yield 'start';
    yield* g;
    yield 'end'
}

new Array(...wrap(genA())) // => ['start', 'a', 'b', 'end']

a generator yielding two values a generator that wraps another generator yield* delegates to another generator note that generators are first class values which can be passed around freely the result is a flat array constructed from the generator values

nice: pass a variable to next:

function* gen () {
    yield 'a';
    const res = yield 'b';
    yield 'c-' + res;
}
const g = gen();
g.next();    // => {value: 'a',   done: false}
g.next('x'); // => {value: 'b',   done: false}
g.next();    // => {value: 'c-x', done: false}
g.next();    // => {value: undefined, done: true}

the first value: 'a' when getting the second value, we pass 'x' into it and get 'b' back leaving 'x' in the generator stored in res now, we get 'c' and whatever we passed in the next step: *'c-x' this way we can control the generators computation!

wrap up: generators

WARNING: opinion ahead

  • they're not useful for iterating lazily through collections
  • they lack library support (lodash, underscore)
  • they don't fit well into existing asynchronous control flows
  • they don't fit well into existing frontend frameworks

wrap up

you cannot use fat arrow functions to declare them

*() => {
    yield 'foo';
}

does not compile sucks when using ES6 classes

lazily reading from a file, stream or other resource

generators don't help you:

  • laziness -> more overhead
  • iterating by byte or line -> even more overhead

better: chunk your file into digestable parts

combining generators to build pipelines

generators don't help you:

  • laziness -> overhead
  • iterating one item a time -> overhead
  • packing and unpacking items -> overhead

combining generators to build pipelines

better:

  • ES6 Array methods
  • reducers
  • Observables, RxJS

the only place I've used a generator so far:

In compilers, for generating unique names for element ids, css classes.

but there is hope

  • generators work also as coroutines
  • they'll let you describe asnychronous workflows
  • without using promises

but that will be another talk

thanks