JavaScript and ES6 Generators

Over the past two months, I have been learning and posting about Elixir, Erlang / OTP, and the Phoenix Framework. I still have a lot to learn about each of these technologies, but the clearest path forward is to start building things — any (and all) the things! However, as a web developer, I cannot build things solely with these technologies — wonderful as they are, for my needs Elixir / OTP / Phoenix Framework must be paired with front-end technologies.

With that intro out of the way, this will be my first post on front-end technologies. I will be using React and its ecosystem, one of which happens to be React Saga. Learning about React Saga led me to JavaScript Generators, and that is what thispost will be about.

What Are Generators

Generators are functions with some very interesting behavior. To fully appreciate how differently generators behave in comparison to regular functions, let’s go over some of the assumptions we make of “plain vanilla” functions first. Let’s use the simply function below as our example.

First Assumption — Functions Run to Completion

One of the main assumptions that we make regarding functions is that, once a function is invoked, and barring any errors, the function will run until it has finished executing. This holds true even when part of a function’s task is to make an async request . The function is not mean to execute the contents of that request — it simply spins up a web worker to do so. The function will run until all of its “tasks” are complete.

Second Assumption — Functions Receive Data Once

Another assumption that we make about functions is that they receive data only once, through their arguments. A function can potentially access state, but retrieving data is not the same as receiving data.

For example, in the greet function above, the argument name is the only data passed into the function. While the function is executing, we can’t pass in more data.

Third Assumption — Functions Return Data Once

Finally, we expect that a function will return a value (even if that value is undefined) only once.

Generators

Now that we’ve made our assumptions / expectations regarding functions clear, let’s create a generator and talk about how generators break these assumptions.

Generator Definition and General Usage

First, let’s define a simple generator in order to get familiar with the syntax:

Notice the asterisk — * — usage here. That’s how we know we’re creating a generator and not a (regular) function. Secondly, notice the use of the keyword yield — we’ll explain this bit in the next section.

Now, when you invoke a generator, we do not actually run the code inside of the generator. Instead, a generator iterator is returned. This iterator has a method called next, and it is the next method, called on the generator iterator, that runs the code we’ve defined in the generator. In other words, we’d use the generator defined above as follows (where gIterator stands for generator iterator:

By calling next on the generator iterator, we received back an object with two properties. Why did the value property hold the value 1? It was a result of using the keyword yield in conjunction with the expression 1. Let’s look at that in more detail.

Generators Run Until Yield

The keyword yield is used to pause a generator. The use of the word pause here is very deliberate. When a generator encounters the keyword yield, it will evaluate the expression to its right, and return that value to the generator’s caller. If there is no expression, then undefined will be returned to the caller. The return value will always be wrapped in an object. This object will tell the caller of the generator iterator what value the generator returned — i.e., what expression was used in conjunction with yield — and whether the generator is done running.

{value: 1, done: false}

We can think of the yield keyword as a generator’s version of the return keyword — with the very important difference that return stops execution, while yield merely pauses it. Next time we call next on the generator iterator, the generator will resume execution “where it left off”. If we were to call next() on the generator iterator again, we’d receive the following:

{value: 2, done: false}

Again, a generator runs until it reaches a yield keyword (or it has nothing else to do). At that point, the generator has not finished execution. It is paused, and execution will resume when the next method is called on the iterator.

Generators Can Receive Data Multiple Times

While regular functions can receive data only once, through their arguments, a generator can receive data from the outside world any number of times.

Take the generator below as an example. Notice how we don’t pass any data to the generator via its arguments, and we don’t have it read the data from state.

First, we’ll create our generator iterator:

var gIterator = nonSequential();

Now, when we call the next method on the generator iterator, it’s going to start executing the code that we wrote. The first thing that it is going to encounter is the yield keyword on line 2. Because there is no expression to the right of yield, the generator will simply return undefined for the value prop:

gIterator.next() // {value: undefined, done: false}

This is now our chance to pass data back into the generator. yield [expr] is itself an expression. What does it evaluate to? It evaluates to whatever we pass to the next method of the generator iterator. For example, if I pass in the number 2, yield will evaluate to 2 on Line 2, and outsideData will hold the value 2 on Line 3.

In other words, we pass data back to a generator with the next method of the generator iterator! We simply call next(arg) to pass in data! It’s up the generator itself to figure out what to do with this data, just as it is up to a function to figure out what to do with arguments.

Generators Can Return Multiple Times

As we saw above, whenever a generator encounters the keyword yield, it will return to its caller the expression to the right of the keyword. This can happen any number of times within a generator. In other words, a generator can return values to the outside world any number of times — as many times as the generator has yields.

How is a Generator Useful?

Generators can be paused by the keyword yield, they can pass data to the outside world multiple times (again via yield), and they can receive data from the outside world multiple times via the generator iterator’s next method. This is all very interesting, but how is this useful in practice?

Well, one thing that generators allow you to do is to write asynchronous code that abstracts away the fact that it is asynchronous. Let’s take the following example code:

Note: Special thanks to Kyle Simpson’s ES6 Generators Series for the above.

I won’t run through all the intricacies of the code above. Instead, let’s focus on the generator main.

This function reads very simply.

  1. You set the result1 variable to whatever yield <expr> evaluates to.
  2. You parse the data held in result1 and use that data to make another request, which will be held in result2.

The request function used in conjunction with the two yield statements makes an asynchronous call. Now, our generator function can’t continue until the AJAX call completes — we need that data. Luckily, yield pauses our generator. As long as our generator is restarted with the right data, we don’t have to worry about anything. (The generator is restarted after the AJAX request completes thanks to a callback function).

We do this twice, for result1 and result2. The complexity of the async call is hidden from the generator, and from the developer. All we need to know is that we are going to pause the generator, yielding whatever request evaluates to. Something else will have to worry about restarting the generator with the correct data so that the generator can continue executing. The beauty of this is that what request does doesn’t actually matter — it can make an async AJAX call, it can look up the data in some kind of store, etc. As long as the next method for this generator iterator is invoked with the data needed, we are good to go!

Summary

As you can see, generators are functions that break our expectations of how functions behave. We can pause generators; we can pass data into generators multiple times; and we can get data out of generators multiple times.

Generators are especially useful in hiding the complexity of async calls. This is only one of its applications — and a crude one at that. Pairing generators with promises, for example, can be a powerful combination, though it is outside the scope of this article.

Resources

  1. MDN JavaScript Generators
  2. MDN Yield Keyword
  3. Kyle Simpson’s ES6 Generators Series