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`*
1 + 2 // => 3 _.range(3) // => [ // 0 // 1 // 2 // ]
comment and fat arrow show the results of an expression works for multiline results too
we'll use one example
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
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.
but
To help writing generators,
ES6 introduced syntax sugar in form of
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?
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!
you cannot use fat arrow functions to declare them
*() => { yield 'foo'; }
does not compile sucks when using ES6 classes
generators don't help you:
better: chunk your file into digestable parts
generators don't help you:
better:
the only place I've used a generator so far:
In compilers, for generating unique names for element ids, css classes.
but that will be another talk