Simplifying async flow control using generators

I’ve been trying to understand generators and their use cases. One of its utilities is in improving control flow of asynchronous operations.

I’m writing this post to understand a code snippet posted under this article - Callbacks vs Coroutines. The article goes into the advantages of using generators and coroutines over callbacks.

The code snippet I want to understand

var fs = require('fs');

// Coroutine
function thread(fn) {
  var gen = fn();

  function next(err, res) {
    var ret = gen.next(res);
    if (ret.done) return;
    ret.value(next);
  }
  
  next();
}

function *generator(){
    var a = yield read('hello.txt');
    var b = yield read('world.txt');
    console.log(a);
    console.log(b);
  }

function read(path) {
  return function(done){
    fs.readFile(path, 'utf8', done);
  }
}

thread(generator);

I have created two files ‘hello.txt’ and ‘world.txt’ with the text, ‘hello’ and ‘world’ respectively.

Output

hello
world

The readFile is an asynchronous operation. The following lines of code

    var a = yield read('hello.txt');
    var b = yield read('world.txt');

are run such that they can be modelled as executing synchronously. The second read operation read('world.txt') does not start until the first completes. And it achieves this without building a callback hell.

The code has three functions

read - It takes as argument the path of the file to be read. It returns a function which takes the callback done for the readFile operation.

function read(path) {
  return function(done){
    fs.readFile(path, 'utf8', done);
  }
}

*generator - This is the generator function with the asynchronous code in its yield statements

function *generator(){
    var a = yield read('hello.txt');
    var b = yield read('world.txt');
    console.log(a);
    console.log(b);
  }

thread - This is the control function responsible for invoking the generator

function thread(fn) {
  var gen = fn();

  function next(err, res) {
    var ret = gen.next(res);
    if (ret.done) return;
    ret.value(next);
  }
  
  next();
}

thread Function execution

function thread(fn) {
    var gen = fn();
  
    function next(err, res) { ...
    }
    
    next();
  }

It creates an iterator by calling fn (which is a generator function). The iterator is stored in the variable gen.

It then invokes the next function without any arguments.

next Function execution

function next(err, res) {
    var ret = gen.next(res);
    if (ret.done) return;
    ret.value(next);
}

var ret = gen.next(res)

=> ret = { value: readFile('hello.txt'), done: false }

=> ret = { value: function(done){ fs.readFile(path, 'utf8', done); }, done: false }

Since ret.done is false, if (ret.done) return; has no affect.

ret.value(next) results in calling the readFile function with next as the callback. Thus when the file is read, next is invoked with the file data as the argument res

next Function execution (as a callback)

When gen.next(res) is invoked, it is invoked with the data from the file hello.txt which was provided as res. This results in var a = 'hello' while the value of ret updates to

var ret = gen.next("hello")

=> ret = { value: readFile('world.txt'), done: false }

=> ret = { value: function(done){ fs.readFile(path, 'utf8', done); }, done: false }

Since ret.done is false, if (ret.done) return; has no affect.

ret.value(next) results in calling the readFile function with next as the callback. Thus when the ‘world.txt’ is read, next is invoked with ‘world’ as the value for the argument res

next Function execution (as a callback)

When gen.next(res) is invoked again, it is invoked with the data from the file world.txt which was provided as res. This results in var b = 'world' and

var ret = gen.next("world")

=> ret = { value: undefined, done: true }

As there are no further yield or return statements inside the generator function.

Since ret.done is true the execution breaks out of the thread function due to if (ret.done) return;