42

Using JavaScript Generators to yield Promises

 5 years ago
source link: https://www.tuicool.com/articles/hit/IzemQfQ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

The moment I started to develop systems in node.js I immediately formed a very bad relationship with callbacks and CPS style of programming. I had three major problems with this style of programming:

1.)My mental model is just not compatible with it. My brain is missing capacity to think in CPS. It's possible that I'm lacking some neurons that could tell me that what I'm doing is right. My brain works in sequential manner and that's how I want to write my programs. Non liner and non-sequential model of callbacks is just a no-go for me.

2.) Inversion of control is IMHO the worst thing that happened to CPS. When using 3rd party vendor libraries, you voluntarily give control of you program to vendor libraries that may or may not call your callback. The vendor library can even call your callback multiple times or not at all. Where is the control and safety here?

3.) Clunky code.This fact is just obvious by looking at any CPS style code. The code offten suffers from effect called pyramid of doom, is very complex, extremely hard to read and prone to bugs. It's like a sea of characters that has nothing to do with your business logic. You can easily release the Zalgo monster if all your functions are not consistently asynchronous.

So right after my short CPS journey, I started to look for an alternative. At the time of my search for alternative the Generators and the Promises become part of node. One time while researching something, I've seen a fragment of code which used promisories (promise producing functions) and Generators to create sequential and linear asynchronous code. I created my own utils around this pattern and some time after that I discovered co . By discovering co I verified that these ideas are valid and are being standardized.

Let's now deep into the theory of using Generators and Promises to handle complex asynchronous flows in sequential manner. Let's say we have a generator function that yields promise instances.

const generator = function* generator() {
  const a = yield Promise.resolve(1);
  const b = yield Promise.resolve(2);

  return a + b;
};

const iterator = generator();
iterator.next(); // { value: Promise(1), done: false }
iterator.next(); // { value: Promise(2), done: false }
iterator.next(); // { value: NaN, done: true }

As you can see generator treats the promises as it treats any other value. No magic happens here and our code behaves not in a way we want it to behave.

But what if we had a function called async that can accept any generator function and create a promisory from it. This async function will understand yielding of the promise and act accordingly.

const promisory = async(function* generator() {
  const a = yield Promise.resolve(1);
  const b = yield Promise.resolve(2);

  return a + b;
});

promisory(); // Promise(3);

You can write entire complex programs using this pattern. Promisories are just functions and they compose. The inner code of this promisories although imperative, is 100x times more readable then CPS. And what's even more important is that the code is now safe (no Zalgo monster), predictable and we eliminated the inversion of control. Also the immutability of the Promises play well with the idioms and the principles of the Functional Programming.

Promisories created in this manner manifest additional behaviors.

Possibility of standard error handling

const promisory = async(function* generator() {
  let a;
  
  try {
    a = yield Promise.reject(new Error('error'));
  } catch (error) {
    a = 2;
  }  
  const b = yield Promise.resolve(2);

  return a + b;

});

promisory(); // Promise(4);

Every error that it thrown from promisory results in the rejection of the resulting promise.

Yield delegation

const helper = function* helper() {
  const a = yield Promise.resolve(1);
  const b = yield Promise.resolve(2);

  return a + b;
}

const promisory = async(function* generator() {
  const a = yield Promise.resolve(1);
  const b = yield Promise.resolve(2);
  const c = yield* helper();

  return a + b + c;

});

promisory(); // Promise(6);

You define your helpers (private API, not exported functions) as pure generators. No need to convert them to promisories. The promisory can use native yield delegation to process the generator. You can avoid additional overhead with this trick.

Promisory delegation

const helper = async(function* helper() {
  const a = yield Promise.resolve(1);
  const b = yield Promise.resolve(2);

  return a + b;
});

const promisory = async(function* generator() {
  const a = yield Promise.resolve(1);
  const b = yield Promise.resolve(2);
  const c = yield helper();

  return a + b + c;

});

promisory(); // Promise(6);

Using one promisory inside another is as easy as calling it. No additional rules involved.

Even recursion delegation is possible, but I don't think demonstrating it here is necessary.

So the big question is how do I implement the async function ? You don't need to. We've already implemented the async function for you in Ramda Adjunct . It's part of 2.16.0 release. It not only generates the promisory from the Generator but the resulting promisory is also auto curried. Let me demonstrate:

import { async } from 'ramda-adjunct';

const promisory = async(function* generator(val1, val2, val3) {
  const a = yield Promise.resolve(val1);
  const b = yield Promise.resolve(val2);
  const c = yield Promise.resolve(val3);

  return a + b + c;
});

promisory(1)(2)(3); // Promise(6)

Word on Async/await

Now if you replace async wrapper function for async expression and yield for await operator you get your long awaited async/await syntax.

const promisory = async function test() {
  const a = await Promise.resolve(1);
  const b = await Promise.resolve(2);

  return a + b;

});

test(); // Promise(3);

The problem is that during the time I was trying to solve the CPS problem, the async/await syntax did not existed yet. There was uncertain plan for it to exist. The current async/await syntax is just a sugar coating on top of Generators and Promises. Our async wrapper function is completely equivalent with the async/await syntax and can be used in environments where Promises and Generators are already part of runtime but async/await syntax is not.

Like always, I end my article with the following axiom: Define your code-base as pure functions and lift them only if needed. And compose, compose, compose…


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK