12/08/2018, 14:41

To Yield or Not To Yield - A layman''s guide to ES6 Generator Functions

Generators One of the most exciting and weired new features of ES2015 are the Generators. How weired you ask? Kyle Simpson, author of the You don't know JS series wrote The name Generator is a little strange, but the behavior may seem a lot stranger on his article. So yeah, 'pretty darn ...

Generators

One of the most exciting and weired new features of ES2015 are the Generators. How weired you ask? Kyle Simpson, author of the You don't know JS series wrote The name Generator is a little strange, but the behavior may seem a lot stranger on his article. So yeah, 'pretty darn weired', I'd say. To put it in layman's terms, Generators are a special breed of fuctions, which can pause its execution by itself for indefinite time, and after that return to the paused part and reexecute.

To directly quote MDN, Generators are functions which can be exited and later re-entered. Their context (variable bindings) will be saved across re-entrances.

Calling a generator function does not execute its body immediately; an iterator object for the function is returned instead. When the iterator's next() method is called, the generator function's body is executed until the first yield expression, which specifies the value to be returned from the iterator or, with yield*, delegates to another generator function. The next() method returns an object with a value property containing the yielded value and a done property which indicates whether the generator has yielded its last value as a boolean. Calling the next() method with an argument will resume the generator function execution, replacing the yield statement where execution was paused with the argument from next().

Availability

At the time of writing this article, node does not directly support Generator functions. node --v8-options | grep harmony

--es_staging (enable test-worthy harmony features (for internal use only))
--harmony (enable all completed harmony features)
--harmony_shipping (enable all shipped harmony features)
--harmony_default_parameters (nop flag for "harmony default parameters")
--harmony_destructuring_assignment (nop flag for "harmony destructuring assignment")
--harmony_destructuring_bind (nop flag for "harmony destructuring bind")
--harmony_regexps (nop flag for "harmony regular expression extensions")
--harmony_proxies (nop flag for "harmony proxies")
--harmony_reflect (nop flag for "harmony Reflect API")
--harmony_tostring (nop flag for "harmony toString")
--harmony_array_prototype_values (enable "harmony Array.prototype.values" (in progress))
--harmony_object_observe (enable "harmony Object.observe" (in progress))
--harmony_function_sent (enable "harmony function.sent" (in progress))
--harmony_sharedarraybuffer (enable "harmony sharedarraybuffer" (in progress))
--harmony_simd (enable "harmony simd" (in progress))
--harmony_do_expressions (enable "harmony do-expressions" (in progress))
--harmony_regexp_property (enable "harmony unicode regexp property classes" (in progress))
--harmony_string_padding (enable "harmony String-padding methods" (in progress))
--harmony_regexp_lookbehind (enable "harmony regexp lookbehind")
--harmony_tailcalls (enable "harmony tail calls")
--harmony_object_values_entries (enable "harmony Object.values / Object.entries")
--harmony_object_own_property_descriptors (enable "harmony Object.getOwnPropertyDescriptors()")
--harmony_exponentiation_operator (enable "harmony exponentiation operator `**`")
--harmony_function_name (enable "harmony Function name inference")
--harmony_instanceof (enable "harmony instanceof support")
--harmony_iterator_close (enable "harmony iterator finalization")
--harmony_unicode_regexps (enable "harmony unicode regexps")
--harmony_regexp_exec (enable "harmony RegExp exec override behavior")
--harmony_sloppy (enable "harmony features in sloppy mode")
--harmony_sloppy_let (enable "harmony let in sloppy mode")
--harmony_sloppy_function (enable "harmony sloppy function block scoping")
--harmony_regexp_subclass (enable "harmony regexp subclassing")
--harmony_restrictive_declarations (enable "harmony limitations on sloppy mode function declarations")
--harmony_species (enable "harmony Symbol.species")
--harmony_instanceof_opt (optimize ES6 instanceof support)

So, we will need to use Babel to transpile our code. For the sake of this article, we will use REPL.it for practicing the examples. Beside every example, you will find a link for the source of that example, and a second link to REPL.it with the code, where you can tinker & tweak with it.

A Quick Example

Hello Yield

Lets consider this example from MDN. You can also try the live demo here.

function* idMaker() {
  let index = 0;
  while (index < 3)
    yield index++;
}

Here we declare a generator function who has an initial index of 0, and will keep sending us data until index is 3. To invoke the it, lets assign the generator function to a variable, and call .next() in it.

const gen = idMaker();

gen.next() // { value: 0, done: false }
gen.next() // { value: 1, done: false }
gen.next() // { value: 2, done: false }
gen.next() // { value: undefined, done: true }

Now when, yeild is faced, the code halts the execution, and returns a Generator Object to the caller. The structure is

{
  value: "What was passed with the yield statement, in our case 'index++'",
  done: "true/false"
}

For the 1st three gen.next() we get the result as expected. For the fourth gen.next() (while is false, in our code), we get the result

{ value: undefined, done: true }

Here, done: true confirms that the generator has ran it's course completely.

Ways to create a Generator

  1. Via a generator function declaration
function* genFunc() { ··· }
let genObj = genFunc();
  1. Via a generator function expression:
const genFunc = function* () { ··· };
let genObj = genFunc();
  1. Via a generator method definition in an object literal:
let obj = {
  * generatorMethod() {
    ···
  }
};
let genObj = obj.generatorMethod();
  1. Via a generator method definition in a class definition (which can be a class declaration or a class expression
  class MyClass {
    * generatorMethod() {
      ···
    }
  }
  let myInst = new MyClass();
  let genObj = myInst.generatorMethod();

From ES6 Generators in depth

Ways to halt/terminate a Generator

  1. A yield, which causes the generator to once again pause and return the generator's new value. The next time next() is called, execution resumes with the statement immediately after the yield.

  2. throw is used to throw an exception from the generator. This halts execution of the generator entirely, and execution resumes in the caller as is normally the case when an exception is thrown.

  3. The end of the generator function is reached; in this case, execution of the generator ends and an IteratorResult is returned to the caller in which the value is undefined and done is true.

  4. A return statement is reached. In this case, execution of the generator ends and an IteratorResult is returned to the caller in which the value is the value specified by the return statement and done is true.

Yield

In plain Englsih Yield means to pause or to surrender or laying down arms. That's why in ye old days knights and sword fighters would shout 'I YIELD! I YIELD!!' whenever in a bad position during a sword fight, and try to find an opportunity to throw sand at the opponents eyes.

An ES6 Generator Function

Now, there were these regular knights, but also, there was A Black Knight who never yields.

Normal JS functions are like the Black Knight, never yeilding to anyone/anything, once initiated, it won't stop until it executes completely.

Regular JS Function

Take this code by Kyle Simpson as an example

setTimeout(function(){
  console.log("I am Late Latif from Comilla");
}, 1); // only 1ms delay, human brain cannot even register differences less than 100ms

function foo() {
  for (var i=0; i<=1E10; i++) {
    console.log(i);
  }
}

foo();

The for loop will take very long to complete. When the loop is being executed, the callback function with 1ms delay cannot execute, and have to wait for the loop to finish. Finally, we get the output

1
2
..
..
1E10

I am Late Latif from Comilla

The yield keyword is used to halt the generator function execution. The value/expression following the yield is also returned as the value attribute of generator objects.

Some more examples

Calling another Generator Function inside a Generator

To call another generator function from inside of a generator function, we need to use the yield* functionName syntax.

Lets consider our 2nd example from MDN

function* generatorB(i) {
  yield i+1;
  yield i+2;
  yield i+3;
}

function* generatorA(i) {
  yield i;
  yield* generatorB(i);
  yield i+10;
}

We have two generator functions here, generatorA takes an input i, yields i immediately, after that calls another generator function generatorB with parameter i, which on it's lifespan halt for 3 times i + [1,2,3], and finally after generatorB's execution ends

var gen = generator(10);

console.log(gen.next().value);  // inside generatorA
// 10
console.log(gen.next().value);  // inside generatorB
// 11
console.log(gen.next().value);  // inside generatorB
// 12
console.log(gen.next().value);  // inside generatorB
// 13
console.log(gen.next().value);  // inside generatorA again
// 20

Uses

Implementing iterators via generators

The following function returns an iterable over the properties of an object, one [key,value] pair per property

function* objectEntries(obj) {
  let propKeys = Reflect.ownKeys(obj);

  for (let propKey of propKeys) {    
    yield [propKey, obj[propKey]];
  }
}

let josim = { first: 'Josim', last: 'Vai' };

for (let [key,value] of objectEntries(josim)) {
  console.log(`${key}: ${value}`);
}


// output
// first: Josim
// last: Vai

Making asynchronous codes synchronous

In the following code, we will use the control flow library co to asynchronously retrieve two JSON files. Note how, in line (A), execution blocks (waits) until the result of Promise.all() is ready. That means that the code looks synchronous while performing asynchronous operations.

co(function* () {
  try {
    let [croftStr, bondStr] = yield Promise.all([  // (A)
        getFile('http://localhost:8000/croft.json'),
        getFile('http://localhost:8000/bond.json'),
    ]);
    let croftJson = JSON.parse(croftStr);
    let bondJson = JSON.parse(bondStr);

    console.log(croftJson);
    console.log(bondJson);
  } catch (e) {
    console.log('Failure to read: ' + e);
  }
});

Conclusion

This was a fun (somewhat?) introduction to the ES6 Generators. While, this post is not vast itself, it takes one of the most confusing topics of ES6 and introduces to its features in byte sized chunks. Also, there are links to some of the in depth articles which you must read to achieve a broader knowledge on this topic.

Study Material

function* on MDN

Yield on MDN

Generator Objects on MDN

Using ES6 Generators And Yield To Implement Asynchronous Workflow

ES6 Generators in depth by PonyFoo

ES6 Generators in depth by Dr. Axel Rauschmayer

The Hidden Power of Generators: Observable async flow control

Why can't anyone write a simple ES6 Generator tutorial

ES6 generators and async/await

And Finally, The Black Knight

0