ES6 Generator Functions


Published: 2017-12-10
Updated: 2017-12-26
Web: http://fritzthecat-blog.blogspot.com/2017/12/es6-generator-functions.html


ES6: ?

There is a new syntax in ES6 for so-called generator-functions. If there is an * (asterisk) behind the keyword function, it is a generator function. The asterisk can be understood as multiplicity, meaning "several values". It can be separated by spaces in any way, like on the following generators *foo(), *bar() and *foobar():

  function* foo() {
    yield "foo"
  }

  function *bar() {
    yield "bar"
  }

  function * foobar() {
    yield* foo()
    yield * bar()
  }
  
  const foobarArray = [ ...foobar() ]
  alert(foobarArray)  // "foo,bar"

A generator function is not a normal function. If you call it, its body is not executed. Instead, an instance of the body is returned, wrapped into an Iterator. Calling the Iterator's next() will iterate the body.

The body must use yield instead of return to output iteration values. The wrapping Iterator will go from yield to yield, until all are done. An optional * (asterisk) behind the yield again stands for multiplicity, i.e. an Iterator is expected to the right of it.

Alternatively to calling next() you can use a for-of loop, apply the spread-operator (like in the example above), or make a destructuring-statement (see examples below).

On the web, you find the sentence

Generators are functions that can be paused and resumed

I think this needs a little more explanation. The Iterator returned from a generator function can go into a wait-for-input state through a yield statement that has nothing on its right side. A next(input) call with a parameter would then move the Iterator to the next yield, whereby the input-parameter is put on the place where the yield was. Should the next yield be an output with a non-empty right side, it will also be performed by this next(input) call.

Let's look at examples to find out what that all means.

ES6 Generator Function Tests

Click onto one of the buttons to get an example script into the text area. Below the script you find an explanation.

Description

Resume

Generator functions come with a lot of abstract words like pausing and resuming, cooperative multitasking, coroutines etc. Looking at the concrete behavior of generator functions may be easier than finding out all about these terms. This is still work in progress. ES7 will introduce an await keyword, so asynchronous processing via generators may change in future.




A generator function does not execute its body when you call it. Instead it returns an instance of its body, wrapped into an Iterator. Each call to the generator function creates a new Iterator instance.

Inside the body, the yield statement must be used instead of return. Should there be a return, the iteration will terminate at that point. Else it will go from one yield to the next, until all are done, always outputting what is right of it.

The example uses a for-of loop to create the test-output.

This example introduces a generator function in a class, and thus the function is called method. The function keyword must be left out (else "SyntaxError: missing : after property id"), so the asterisk looks a little lonely in front of the *keyValueTuples() generator method.

The example class ObjectAnalyzer yields all property / value tuples of an object given in constructor, as an array of two. The for-of loop receives that array in a [ key, value ] destructuring.

When a generator function contains a yield without argument, this waits for input through a next(argument) call. That means, it puts whatever is passed as argument to next() in place of the yield.

This example defines a generator function that has three yields, the first and the second being inputs, the last being an output.

First we must call an empty next() to get to the first yield of the "newborn" iterator. We could call it the "start"-next, any parameter will be ignored. The next("Hello") then goes to the local constant first, and next("World") to second constant. There, at the third call, also the output-yield is done, and we receive "Hello World" from this call.

Try to replace the yield first+" "+second by return first+" "+second. You would fail in the helloWorld.done === true condition, and you would have to change it to helloWorld.done === false. That means, a return triggers done === true, but the last yield does not do such. See also example "End of iteration return".

A yield with a trailing * (asterisk) expects an Iterable to the right of it. That means, the generator function body will stop at the yield statement, output the first value of the Iterable to the right, on next call output the next value, and so on.

In the example, this is shown twice. The arrayIterator() generator uses the yield* to output all elements of a given array parameter. The delegationGenerator() generator uses it to output all values yielded by arrayIterator(). A while-loop is used to create the test-output.

Of course this example is not really useful, but it shows that yield* can go to any depth.

This example tries to make clear the role of a return statement in a nested generator function. The *ab() generator yields "a" and "b", additionally it returns a string "End of iteration return".

An iterator is retrieved from *ab(). When we would destructure or spread it, or loop it in a for-of, we wouldn't get the "End of iteration return", because these instructions ignore the last iteration element that has done === true. We can get the return just when looking at the last iteration element, by reading away all elements with done === false, and then retrieving the value from the last one.

Mind that a return terminates the Iterator, any yield after it makes no sense and will be silently ignored.

This example summarizes the ways how the iterator from a generator function can be iterated. The most elegant way is most likely the for-of loop. The spread-operator does not provide terminating the iteration prematurely, it will always retrieve all elements. Destructuring may not always be the best solution, because it makes assumptions about the return that may not hold. The longest one surely is the while-next loop, but it is the only one that is able to also receive an optional return value.


ɔ⃝ Fritz Ritzberger, 2017-12-10