You are here

From Javascript Callbacks to Promises to Generators and Coroutines

Corey Pennycuff's picture

JavaScript has always been a powerful language, but it is not always pretty to look at. ES6 has also greatly improved the syntax of the language, and although it's not perfect, it's not that bad, either. Some of JavaScript's limitations are also its strengths, which is what we will see when we examine the JavaScript coroutine pattern.

Javascript is single-threaded. It is also event-driven, so that many of its functions use a callback pattern. Unfortunately, the callback pattern is ugly and cumbersome in code. Promises improved the callback pattern slightly, but it is still not as clean as it could be. The generator pattern is the latest incarnation of data-flow syntax. To get an intuitive feel for the differences between the different approaches, look at the following code examples:

The callback method:

fs.readFile('filename.txt', 'utf8', function(err, data) {
  if (err) {
    // Something failed.
  }
  else {
    // Do something with 'data'.
  }
});

We can turn the "callback" version of fs.readFile() into a Promise-based version using the promisify() function from my previous blog post.

let readFile = promisify(fs.readFile);
readFile('filename.txt', 'utf8')
  .then(data => {
    // Do something with 'data'.
  })
  .catch(err => {
    // Something failed.
  });

Before we look at the coroutine version of this code, consider the synchronous function approach using fs.readFileSync():

try {
  let data = fs.readFileSync('filename.txt', 'utf8');
  // Do something with 'data'.
}
catch (e) {
  // Something failed.
}

This version is perhaps the cleanest version yet, but it does have an important drawback: It is synchronous. That means that all code execution stops and waits until the synchronous call completes before moving on. While it makes for some clean code, it is also inefficient. In all of the other examples, code from other events are able to run while the file I/O is performed, then the code execution resumes (either the callback is executed or the .then() of the Promise) once the I/O is finished. In other words, the synchronous code comes at the cost of wasted time.

Look at the coroutine approach, which uses a coroutine() function (which we will write over the course of this post) and the promisify() function (mentioned earlier) to turn the generator into a beautiful, simple, data-flow syntax.

let readFile = promisify(fs.readFile);
coroutine(function* (){
  let data = yield readFile('filename.txt', 'utf8');
  if (data instanceof Error) {
    // Something failed.
  }
  else {
    // Do something with 'data'.
  }
})();

The coroutine example is now the most beautiful and straightforward example. It is even easier to read than the synchronous example, due to the lack of the try..catch block. It reads naturally and is easy to follow. Some complicated logic flows would be difficult or impossible to write using callbacks or Promises, but coroutines remove all of the clumsy syntax and leave a syntax that is as easy to use as standard synchronous programming. In fact, the only odd parts about it are the asterisk (*) in the function identifier and the yield keyword.

Generators

The first part of writing a coroutine() function is to understand what a JavaScript generator is. Many people have already written good descriptions about how they work, so I'm not going to go into great detail about them. I will give a bit of surface knowledge, though.

A generator function is denoted by an asterisk after the function keyword. Take the following generator example:

function* plusTwo(i) {
  while(1) {
    yield i;
    i += 2;
  }
}

When executed, plusTwo() accepts a starting value (i) and returns a generator instance. It should be noted that plusTwo() can be executed many times, each time returning a different generator instance.

let counter1 = plusTwo(1);
let counter2 = plusTwo(1337);

For each of these generator instances, we can repeatedly call .next(), which returns an object that tells us two things: whether or not the generator has completed, and what value the generator is putting out at this time.

But what value does the generator give? That depends on what is to the right of the yield keyword. In our plusTwo() example, i is to the right of yield, and it is incremented by 2 every time the loop executes. Look at how the value increases as i increases and is repeatedly yielded.

counter1.next().value; // 1
counter1.next().value; // 3
counter1.next().value; // 5
counter2.next().value; // 1337
counter2.next().value; // 1339
counter1.next().value; // 7

Realize what just happened: The code in the generator function ceased execution until .next() was called.

But generators have one more little trick. If you can get a value out of a generator by putting something on the right side of the yield keyword, you can pass something back into the generator by providing an argument to the .next() function call, and that argument will be given to whatever is to the left of the yield.

Let me say this another way, perhaps with an example using Promises:

let readFile = promisify(fs.readFile);
function* exampleGenerator() {
  let result = yield readFile('filename.txt', 'utf8');
}

In this example, readFile() returns a promise, which is given to the yield. Suppose now that, once the promise is resolved, we call the .next() function, but pass in the result of the promise. Such a solution might look like this:

let readFile = promisify(fs.readFile);
function* exampleGenerator() {
  let result = yield readFile('filename.txt', 'utf8');
}
let gen = exampleGenerator();
gen.next().value.then(data => gen.next(data));

This is the important paradigm shift! Many programmers think of generators as something that their main program uses to create a sequence. In this example, though, the "main program" was inside the generator, and the outside code served as a driver to take the Promise returned by the generator, wait for it to resolve, and then pass the data back into the generator.

Look at the last line of code again. gen.next().value is a Promise, because that is what what returned by the function to the right of the yield. When the Promise is fulfilled, the .then() callback is executed, which in turn feeds the result back into the generator.

"But this only works for one Promise!" you say. You are right. We should be able to create a function, though, that can take any generator and "wrap" it so that it continues this asynchronous resolution continuously, until the generator terminates.

That is exactly the purpose of the coroutine function. So let's build it!

Building a Coroutine function

The shell of our coroutine() function will take a generator as an argument and return a function that can be executed at a later time. When the function is executed, it should pass its arguments into the generator function.

function coroutine(fn) {
  return function() {
    let gen = fn(...arguments);
  }
}

It is important to keep straight what the different parts are. fn is a generator function that is provided to coroutine(). coroutine() returns an anonymous function that can be executed later. When it is executed, fn (the generator) is called with the initialization arguments, and a reference to the initialized generator (gen) is stored. Now, we just need call gen.next() over and over, resolving asynchronous code and passing it to the next gen.next(). We will do this with a local function. Dealing with Promises is a bit cumbersome, however, so let's start with something easier.

function coroutine(fn) {
  return function() {
    let gen = fn(...arguments);
    next();
    function next(result) {
      let yielded = gen.next(result);
      if (!yielded.done) {
        next(yielded.value);
      }
    }
  }
}
 
coroutine(function* simpleYieldValue() {
  let x = yield 3;
})();

Look at the yield in simpleYieldValue(). The value 3 is yielded, which is returned to the coroutine() function. This value is then passed to next(), which is fed back into gen.next(). It is important that you take the time to understand this exectuion flow. Don't continue reading until you have a firm grasp on how this code works.

There is only one more type of value that our coroutine() function needs to work with. It currently only handles synchronous values. What if the value given to yield, though, is a Promise? The answer is actually quite elegant.

function coroutine(fn) {
  return function() {
    let gen = fn(...arguments);
    next();
    function next(result) {
      let yielded = gen.next(result);
      if (!yielded.done) {
        if (yielded.value instanceof Promise) {
          yielded.value
            .then(function(data) {
              next(data);
            },
            function(err) {
              next(err);
            });
        }
        else {
          next(yielded.value);
        }
      }
    }
  }
}

In this most recent incarnation, if yielded.value is a Promise, then all that we need to do is to add a .then() to the yielded.value, and provide success and error handlers, each of which simply call next() again and pass on the value. Here I have made a decision to pass the error value back to the generator in the same way that a success value would be passed back, a method which I prefer over try..catch blocks.

There is one last improvement to make. I would like for the return value of coroutine() (which is a function), when executed, to return a Promise itself. There is a two-fold benefit to this. First of all, it would allow you to run code after the coroutine has finished via .then(). Secondly, it would allow a coroutine to be yielded, and therefore nested one inside another. The change to the code isn't that difficult, either.

function coroutine(fn) {
  return function() {
    let gen = fn(...arguments);
    return new Promise(function(accept, reject) {
      next();
      function next(result) {
        let yielded = gen.next(result);
        if (!yielded.done) {
          if (yielded.value instanceof Promise) {
            yielded.value
              .then(function(data) {
                next(data);
              },
              function(err) {
                next(err);
              });
          }
          else {
            next(yielded.value);
          }
        }
        else {
          if (yielded.value instanceof Error) {
            reject(yielded.value);
          }
          else {
            accept(yielded.value);
          }
        }
      }
    });
  }
}

The version of the code that we just wrote will do everything that we need, but it's a bit verbose for my taste. Because we are using ES6, we can reduce the amount of boilerplate code thanks to the arrow function syntax:

function coroutine(fn) {
  return function() {
    let gen = fn(...arguments);
    return new Promise((accept, reject) => {
      next();
      function next(result) {
        let yielded = gen.next(result);
        if (!yielded.done) {
          if (yielded.value instanceof Promise) {
            yielded.value.then(data => next(data), err => next(err));
          }
          else {
            next(yielded.value);
          }
        }
        else {
          if (yielded.value instanceof Error) {
            reject(yielded.value);
          }
          else {
            accept(yielded.value);
          }
        }
      }
    });
  }
}

A ternary operator (one of my favorite code syntax tools) further condenses and tidies the code:

function coroutine(fn) {
  return function() {
    let gen = fn(...arguments);
    return new Promise((accept, reject) => {
      next();
      function next(result) {
        let yielded = gen.next(result);
        if (!yielded.done) {
          yielded.value instanceof Promise ? yielded.value.then(data => next(data), err => next(err)) : next(yielded.value)
        }
        else {
          yielded.value instanceof Error ? reject(yielded.value): accept(yielded.value);
        }
      }
    });
  }
}

Better yet is a nested ternary.

/**
 * Convert a generator into a coroutine.
 **/
function coroutine(fn) {
  return function() {
    let gen = fn(...arguments);
    return new Promise((accept, reject) => {
      next();
      function next(result) {
        let yielded = gen.next(result);
        !yielded.done ?
          (yielded.value instanceof Promise ? yielded.value.then(data => next(data), err => next(err)) : next(yielded.value)) :
          (yielded.value instanceof Error ? reject(yielded.value): accept(yielded.value));
      }
    });
  }
}
 
 
/**
 * Example usage.
 **/
// Convert a callback function to a promise function.
let readFile = promisify(fs.readFile);
 
// Convert a generator into a coroutine.
let program = coroutine(function* (filename){
  let data = yield readFile(filename, 'utf8');
  if (data instanceof Error) {
    // Something failed.
  }
  else {
    // Do something with 'data'.
  }
});
// Kick of the coroutine and pass in a filename.
program('filename.txt');

I provided an example generator that takes the filename as an argument, which you can see is but a trivial addition. It is important to remember that the yield keyword only works with a single value at a time, and that the value must either be a value of some sort or a Promise. It does not work with callback-based functions. As shown in the example above, however, it is also quite easy to use a promisify() function (provided elsewhere in this blog) to convert callback functions into a Promise, meaning that just about every library can now be used in a coroutine.

Tags: