Morgan Conrad

ES6 Generators for the Nosy

Tags: all JavaScript ES6 generators     Aug 13 2017

JavaScript ES6 adds a new construct called a generator. They appear function-like, but don't be fooled, there are a lot of differences. In fact, the more you know the less like a "function" they seem.

Notes

  • Links prefaced by the "experiment" icon experiment indicate "less frequently asked questions" and are covered in the Appendix.
  • In the "printouts", in many cases newlines have been replaced by spaces for brevity.

Syntax

Declaration

The declaration line for a generator has a *, which can be either

 1) function *foo() { ... }
 2) function* foo() { ... }
 3) function * foo() { ... }
 4) function*foo() { ... }

Kyle Simpson and others make a good case to version 1 over version 2, so I'll use that. Versions 3 and 4 are just silly.

yield

Within a generator, (experiment how about in a normal function?) one can use a new keyword, yield. This allows for two way communication (more later). A "typical" syntax is:

var incoming = yield outgoing;

As in a return statement, outgoing is optional, but for a generator it really should be present almost all the time.
The left side, var incoming, is truly optional and will often be absent.

IMO, it's unfortunate that yield gets used for two purposes: outgoing and incoming communication. I would have preferred something like


yield outgoing  
var incoming = resume  

or even


return outgoing  
var incoming = resume  

which might costs one more reserved word but provide a ton of clarity. But the ES6 usage of yield is consistent with several other languages. "Clever" programmers can actually make the yield line very messy with more operations. IMO it's confusing enough already, and the operator precedence is wonky, so don't do that!

(experiment Can I use return in a generator?)

What the Heck is going on?

Even though *foo() looks like a function, it isn't. Proof:


function *foo(x) {  
   console.log('starting foo');
}

var it=foo(0);  

Prints nothing. It looks like you are "calling" foo(), but something else is happening. "Calling" a generator does not run the code in the generator. It produces an iterator (a.k.a. "generator iterator") that you then use to control the generator and execute it's code. (experiment Can I use new foo() to get the iterator?)

What does yield outgoing do?

Each "yield" returns the result of one iteration.

What am I iterating?

In the example above, not much. However, a real generator iterates by yielding a result to it.next(). And resumes the next time you call it.next().

Here's a minimal generator, returning the value you passed in:

function *foo(x) {  
   yield x;
}

var it=foo('42');  
console.dir(it.next());  
console.dir(it.next());  

The results are:

{ value: '42', done: false }
{ value: undefined, done: true }

Here's a slightly more useful generator, iterating over a range of integers. We use the new-fangled ES6 for of loop to access the values.

function *foo(min, max) {  
   while (min < max)
      yield min++;
}

for (var t of foo(0, 5))  
   console.dir(t);

Which prints 0 1 2 3 4 as expected.

Can I "call" another generator?

Yes, including yourself. yield * (with optional spaces in the 4 versions from above) delegates to another iterable. (experimentdo they have to be a generator?) Let's "enhance" our range iterator so it recurs with max-1:

function *range_r(min, max) {  
   while (min < max)
      yield min++;

   if (max > 0)  // important!
      yield *range_r(0, max-1);
}

for (var v of range_r(0, 4))  
   console.log(v);

prints: 0 1 2 3 0 1 2 0 1 0

If you forget that if (max > 0) // important! line, it will print the same results, because it is no longer yielding anything to print, but generators cannot defy laws of programming: the code still goes into an infinite loop and a stack overflow.

What funky stuff can I do?

You can provide an infinite iterator. For example, for a lot of squares starting at x:

function *foo(x) {  
   while (true) {
      yield x*x;
      x++;
   }
}

This means that the caller has to know when to stop:

var it=foo(2);  
var t = it.next();  
while (t.value<=25) {  // stop myself  
   console.dir(t);
   t = it.next();
}

result :

{ value: 4, done: false }
{ value: 9, done: false }
{ value: 16, done: false }
{ value: 25, done: false }

In theory one can also stop the iterator by calling return() but that didn't work for me.

What about passing data back to the Generator?

Here's a variation on the above, where one can (optionally) pass back in x.

function *foo(x) {  
   while (true) {
      var newX = yield x*x;
      x = newX || x;  // switch x to y if one was passed in
      x++;
   }
}

var it=foo(2);  
console.dir(it.next());  
console.dir(it.next());  
console.dir(it.next(5));  

result:

{ value: 4, done: false }
{ value: 9, done: false }
{ value: 36, done: false }

All these examples are standalone functions. Can I use them in an object or class?

Generators within a Javascript object

The version 1 syntax works great with the new ES6 concise methods. If you prefer, you can still use old fashioned function syntax - note the placement of the "*".

var foo = {  
   max : 5,

   *generator() {  // new concise syntax
      var x = this.max;
      while (x > 0)
         yield x--;
   },

   traditional: function*() {
      yield *this.generator();
   }
};

foo.max = 3;  
for (var v of foo.generator())  
   console.log(v);
for (var v of foo.traditional())  
   console.log(v);

prints 3 2 1 and 3 2 1.

Generators within a pre-ES6 "class"

Again note the placement of the "*".

function Foo(x) {  
   this.x = x;
}

Foo.prototype.generator = function*(){  
   var x = this.x;
   while (--x > 0)
      yield x;  
};

Foo.prototype.anotherGenerator = function*() {  
   yield *this.generator();
};

var foo = new Foo(5);  
for (var v of foo.generator())  
   console.log(v);
for (var v of foo.anotherGenerator())  
   console.log(v);

prints 4 3 2 1 then 4 3 2 1.

Are Generators Useful?

The examples here range from useless to very slightly useful. Surely they didn't add all this syntax just to do ranges of squares?

Generators are powerful tools and I'll discuss more uses in a later post. In the meantime, to the laboratory!


Appendix: Experiments


lab Can I use `yield` in a normal function?



No.

function notAGenerator() {  
   yield 42;
}

var z = notAGenerator();  

Will complain:

morgan@morgan-UL80Jt ~/Work/ES6 $ node test1.js  
/home/morgan/Work/ES6/test1.js:13
   yield 42;
         ^^

SyntaxError: Unexpected number  

lab Can I use `return` in a generator?



Yes, but...

function *foo(x) {  
   yield x;
   return -1;
}

var it = foo(4);  
console.dir(it.next());  
console.dir(it.next());  

prints:

{ value: 4, done: false }
{ value: -1, done: true }

but, return doesn't work with for..of loops!

for(var v of foo(4))  
   console.dir(v);

Only prints "4". Use return with extreme caution!


lab Can I use `new` to create the generator iterator?



Some blogs that imply that you can't but it works for me:

function *foo(x) {  
   yield x;
}

var it = new foo(4);  
console.dir(it.next());  
console.dir(it.next());  

prints what you expect:

{ value: 4, done: false }
{ value: undefined, done: true }

lab Can I yield * to a non-generator?


Yes, you can delegate to any iterable, such as an array, string, or Map. (But not object or null!)

function *array_string_map(array, string, map) {  
   yield * array;
   yield * string;
   yield * map;
}

for (var v of array_string(  
       [1,2,3,4],
       'abcd',
       new Map([ [{id: 'foo'}, 'bar'] ])
     ))
   console.log(v);

generates:

1 2 3 4 a b c d [ { id: 'foo' }, 'bar' ]