LoginSignup
20
20

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-05-12

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 と比べてみる

20
20
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
20