Yield and Async notations

Yield and Async notations

By Guillaume Claret

The yield and async notations aim to improve the code readability in asynchronous JavaScript and iterators. In this article, we attempt to present these notations through the concepts of monad and continuation. From there we show how yield and async are closely related.

Yield and Async

We quickly recall how yield and async work on some examples.

The notation yield allows to write generators. The generators are special functions which can pause their execution on a yield and resume on a .next(). We can also compose generators with yield*:

function* g1() {
  yield 2;
  yield 3;
}
function* g2() {
  yield 1;
  yield* g1();
  yield 4;
}

const generator = g2();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: 3, done: false }
console.log(generator.next()); // { value: 4, done: false }
console.log(generator.next()); // { value: undefined, done: true }

The notation async helps to write asynchronous operations in a synchronous-like manner. For example we can sequence API calls using the await keyword:

async function foo(param) {
  const firstResult = await firstApiCall(param);
  return 1000 * await secondApiCall(firstResult, param);
}

Monad

Let us dive into some definitions. A monad is a data structure representing some computations. These computations are usually non-purely functional (meaning that they do side-effects).

The main monadic operator is bind and executes two computations in sequence. The promises are a well-used form of monad. The sequencing operator of the promises is .then:

promiseA.then(result => promiseB(result));

Now you know what a monad is!

Continuation

The continuation of an expression is a function representing which computations remain to be done to terminate the current function (or program). For example in:

async function foo(param) {
  const firstResult = await firstApiCall(param);
  return 1000 * await secondApiCall(firstResult, param);
}

the continuation of await firstApiCall(param) to terminate the function foo is the code represented in green:

async function foo(param) {
  const firstResult = await firstApiCall(param);
  return await secondApiCall(firstResult, param);
}

which expressed as a function becomes:

async function continuation(firstResult) {
  return 1000 * await secondApiCall(firstResult, param);
}

Putting everything together

Let us draw a parallel between the yield and async notations as follows:

  yield notation async notation
sequencing yield* await
primitive operator yield new Promise

We sequence generators with the keyword yield* and promises with await. Using our previous definitions, we can say that:

  • yield* is a syntactic sugar to apply the monadic sequencing operator of the generators to the current continuation;
  • await is a syntactic sugar to apply the monadic sequencing operator of the promises to the current continuation.

If we take back our async example:

async function foo(param) {
  const firstResult = await firstApiCall(param);
  return 1000 * await secondApiCall(firstResult, param);
}

we can desugar the first await into a .then applied to the continuation:

function foo(param) {
  return firstApiCall(param).then(async firstResult => {
    return 1000 * await secondApiCall(firstResult, param);
  });
}

If we desugar the second await we get:

function foo(param) {
  return firstApiCall(param).then(firstResult => {
    return secondApiCall(firstResult, param).then(result =>
      1000 * result
    );
  });
}

which is what we would write without the async notation.

The power of the yield

The operators yield* and await use the current continuation at compilation time, as we can view them as syntactic sugars 1. In constrast, the yield operator gives access to the continuation at run time. We apply the current continuation returned by yield with the .next method:

function* gen() {
  const x = yield;
  return 10 * x;
}

const generator = gen();
generator.next(); // warm up the generator
const result = generator.next(12); // apply the continuation of the `yield` on `12`
console.log(result.value); // 120

In particular, we can implement the async notation at run time with the yield, by doing a .then with the current continuation returned by the .next. This is exactly what some libraries like co do. By applying the same technique, we can actually implement an async-like notation for most monads thanks to the yield operator 2. For the fun of it, we give a full example in this JSBin for the state monad.

To summarize

The notations yield and async simplify the definition of impure functions. The key operators are yield* and await which sequences two computations. More precisely, these operators apply the monadic sequencing combinator bind to the current continuation of an expression.

The functions async create promises, but the generators are more generic. Indeed, at runtime, a generator can be interpreted to any side effect thanks to the yield operator which returns the current continuation.

  1. In practice, these notations are implemented as state machines rather than as syntactic sugars in Babel.

  2. Up to the fact that the continuation returned by yield can be called only once.

OuiCar's Picture

About OuiCar

OuiCar is a community market place to find and rent cars anywhere in France. But we are not just car renters. We also like to experiment new technologies and share ideas with others.

Paris http://www.ouicar.fr

Comments