3 cases where JavaScript generators rock (+ understanding them)

20 Oct 2016

One of the new features in ES6 is a new type of function, a generator function. You’ve probable heard of them.

They are defined almost like normal functions, but instead of function, you use function*. Also in the body, a new keyword, yield can be used to return a stream of values from the generator.

A lot of articles on generators, however, focus purely on what generators technically are, and not so much on what they enable and how they’re used in real apps.

In this article, I would like to present a thinking pattern that helps understand generators, as well as a few real-world examples, which are the second half of the post.

No more trying to learn and giving up because things felt… too complex and almost out of touch with reality.

Understanding

First… What’s a function, an old, regular function?

It’s a sequence of commands that are executed, one by one, when that function is called. The function can communicate some value to its caller by using return.

function regular() {
  doA(); // execute this
  doB(); // execute that
}

Straightforward.

What generators bring to the table is the ability to treat a function like a program — that can be executed according to some specific rules that you define. So let’s call a generator function a program.

To execute a program, though, we do need an interpreter, that will give that special behavior we want. It’s going to take the program in and run it:

interpreter(function* program() {
  // do something
});

yield command is how a program reaches out to the interpreter.

It is a two-way communication channel between a program and an interpreter:

  1. a program sends something to an interpreter
  2. the interpreter does its thing and can send something back to the program

For example, here is how we send a command b to the interpreter and put its response into a.

const a = yield b;

It’s important to note that this call is not necessarily synchronous, the interpreter can give us a right away, but it can also decide to wait and give it to us in 5 minutes. The generator function will pause and not execute further until the interpreter tells it to.

Okay… nuff theory. Let’s show some real-world use-cases, shall we?

Examples!

1. Linear build tests for async tool

Brunch is a front-end bundler. Not surprisingly, it has a “watch” mode when it re-builds every time a file changes.

To test this behavior end-to-end, we basically need to:

I utilized the power of generators to make this kind of testing easy-peasy:

watch({}, function* (compilation) {
  yield compilation(); // pause and wait
  t.false(fs.readdirSync('./node_modules').includes('lodash'));

  // make changes
  const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
  packageJson.dependencies.lodash = '*';
  fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2));

  yield compilation(); // pause and wait
  // assert they were reflected
  t.true(fs.readdirSync('./node_modules').includes('lodash'));
  t.end();
});

As you can see, it reads pretty linearly and naturally.

In this case, watch is an interpreter which allows us to pause and wait for the compilation to finish. If you are wondering how it’s implemented: simple.

The generator function is a program which ensures the watcher is working properly.

(The non-generator version of this test used to have a callback with a counter, pretty messy: commit.)

2. co

co is a generator interpreter which allows you to write nicer, more linear code for promises.

Instead of this nested code:

getUser().then(user => getComments(user))

You have this:

co(function* () {
  const user = yield getUser();
  const comments = yield getComments(user);
});

Basically, async-await implemented in the userland.

3. redux-saga

redux-saga makes managing side-effects in the Redux architecture a breeze.

Instead of scattering them across dozens of action creators and reducers, you group logically related pieces of behavior in a program called saga.

Redux-saga can interpret many commands. Among the most popular are:

An example would be:

function* welcomeSaga() {
  yield take('REGISTRATION_FINISHED');
  yield put(showWelcomePopup());
}

sagaMiddleware.run(welcomeSaga);

(I have a concrete example of Redux-Saga application to improve user experience.)

Conclusion

As can be evidenced by the examples, generators are a really powerful tool that lets you have cleaner code — especially when it comes to any kind of async behavior.

A side benefit of generators is easier testing. For example, you could test a program by writing a different interpreter, that asserts the yielded commands without executing them.

Further reading

You can learn more about generators and their inner workings here.


If you liked this and want to read about redux-saga in particular, subscribe below to be the first to know about my new React & Redux posts.

Think your friends would dig this article, too?

Google+
Tumblr