Help us understand the problem. What is going on with this article?

ES2017におけるasyncとgenerator、Promise、CPS、モナドの関係

More than 1 year has passed since last update.

ES2016におけるasyncとgenerator、Promise、CPSの関係

次の非同期sleepをES2016ではどう書けるかを比較する。

// sleep: number -> Promise<number>
function sleep(ms){
  return new Promise(resolve =>
    setTimeout((()=>resolve(ms)), ms));
}

async-await版

async function main(){
  let a = await sleep(1000);
  alert(`${a}ms passed`); // 1000ms passed
  let b = await sleep(2000);
  alert(`${b}ms passed`); // 2000ms passed
  let [c, d] = await Promise.all([
     sleep(3000),
     sleep(4000)
  ]);
  alert(`${Math.max(c, d)}ms passed`); // 4000ms passed
  alert('done');
}

main(); // return Promise

async-await記法を使うとスッキリ書ける。
非同期処理の同期待ちには Promise.allawait すればよい。
このコードはgeneratorを使うと次のように書ける。

generator版

// main: void -> Promise<void>
let main = async(function* _main(){
  let a = yield sleep(1000);
  alert(`${a}ms passed`);
  let b = yield sleep(2000);
  alert(`${b}ms passed`);
  let [c, d] = yield Promise.all([
     sleep(3000),
     sleep(4000)
  ]);
  alert(`${Math.max(c, d)}ms passed`);
  alert('done');
});

// async: (void -> Generator) -> (void -> Promise)
function async(generatorFunc) {
  let generator = generatorFunc();
  let onResolved = arg =>{
    let result = generator.next(arg);
    if (result.done) {
      return result.value;
    } else {
      return Promise
      .resolve(result.value)
      .then(onResolved);
    }
  }
  return onResolved;
}

main(); // return Promise<void>

generator関数である _mainyeild sleep(3000)Promise<number> 返す。
その Promise<number>async 関数が成功か失敗かを判断し、成功ならば次の計算を呼び出すことで、async-awaitとおなじようにフラットに書ける。

つまり async 関数は 内部で Promise のチェーンをつないでいるのだ。

Promise版

// main: void -> Promise<void>
function main(){
  return sleep(1000).then((a)=>{
    alert(`${a}ms passed`);
    return sleep(2000).then((b)=>{
      alert(`${b}ms passed`);
      return Promise.all([sleep(3000), sleep(4000)]).then(([c, d])=>{
        alert(`${Math.max(c, d)}ms passed`);
        alert('done');
        return;
      });
    });
  });
});

main(); // return Promise<void>

これが generator 版で async 関数が作っていた Promise のチェーンである。
重要な事実だが Promise では コールバック地獄を防ぐことはできない

async function main(){
  const a = await getA();
  const b = await getB();
  return a + b;
}

のような処理を Promise で書こうとすると

function main(){
  return getA().then((a)=>
    getB().then((b)=>
      a + b));
}

というように以前の前の Promise の返り値を利用するためにはクロージャを利用する必要があるため
ネストを深くせざるを得ないからである。
ではなぜ Promise をつかうのかというと、 then メソッドや Promise.all, Promise.race を使うことで非同期処理の演算ができるようになり、コールバックスタイルよりも表現力が上がるからである。

最後に sleep を古き良きコールバックスタイルにし、これを継続渡し形式(CPS)で書いてみる。

CPS版

// sleep: number -> (number -> void) -> void
function sleep(ms, cb){
  setTimeout((()=> cb(ms)), ms);
  return;
}
// main: void -> Promise<void>
function main(){
  sleep(1000, (a)=>{
    alert(`${a}ms passed`);
    sleep(2000, (b)=>{
      alert(`${b}ms passed`);
      let waitAll = genWaitAll(next);
      sleep(3000, waitAll());
      sleep(4000, waitAll());
      function next([c, d]){
        alert(`${Math.max(c, d)}ms passed`);
        alert('done');
        return;
      }
    });
  });
};

// genWaitAll: ([a] -> void) -> (void -> (a -> void))
function genWaitAll(next){
  let results = [];
  let counter = 0
  return ()=>{
    let i = counter;
    counter++;
    return (ms)=>{
      results[i] = ms;
      counter--;
      if (counter === 0){
        next(results);
      }
    };
  }
};

main(); // return void;

callback hell が起きている。
また、 Promise.all のやっていた同期待ち処理を genWaitAll のように手書きする必要がある。

モナドとか

JavaScript + generator で Maybe、 Either、 Promise モナドと do 構文を実装し async-await と比べてみる

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした