そんな…どうして…?さやかちゃん、async/awaitでPromiseから人を守りたいって、そう思って魔法少女になったんだよ。


async/awaitの話も入れてほしいというご要望があったので、続きを書きました。
前回記事:今日までコールバックと戦ってきたみんなを、Promiseを信じた魔法少女を、私は泣かせたくない。

(※なぜか『魔法少女まどかマギカ』の一部ネタバレが含まれるので注意してください)

人物紹介 (適当)

  • まどか
    • Promiseで戦う魔法少女。歴代コールバック対策の全てを無かったことにしようと目論む。
  • ほむほむ
    • 12年間ずっとまどかにPromiseの使い方を教えてきた謎の魔法少女。銃火器やロードローラーで戦う。
  • マミさん
    • 目新しい技術を手にすると何も恐くなくなって死ぬ。
  • さやかちゃん(笑)
    • コールバックやPromiseが嫌いだったが、async/awaitで魔法少女になった。
  • 杏子
    • クロージャのthisについて調べている途中で頭がおかしくなった父親を持つ、心優しい魔法少女。

アンタが噂のasync/awaitってやつか。妙な技を使いやがる。

まずはPromiseasync/awaitで順次実行を比較。

const results = {};
Promise.resolve()
  .then(() => $.get('hoge.txt')).then(hoge => results.hoge = hoge)
  .then(() => $.get('piyo.txt')).then(piyo => results.piyo = piyo)
  .then(() => console.log(results.hoge + results.piyo));
(async() => {
  const hoge = await $.get('hoge.txt');
  const piyo = await $.get('piyo.txt');
  console.log(hoge + piyo);
})();

構成は似ていますが、async/await側は劇的にコード量が減っています。
Promise側は本来、変数resultsのスコープを閉じるためにさらにコード量を増やす必要がありますから、async/await恐るべしです。

でも、マミさんが言ってました。
危険を冒してまで叶えたい願いがあるのかどうか、じっくり考えてみるべきだと思うの、って。(その後すぐ死んだ)

さあ、行こう。今夜もPromiseをやっつけないと。

次にPromiseasync/awaitで、並列実行と順次実行の混在を比較。

const results = {};
Promise.all([
  Promise.resolve().then(() => $.get('hoge.txt')).then(hoge => results.hoge = hoge),
  Promise.resolve().then(() => $.get('piyo.txt')).then(piyo => results.piyo = piyo)
])
.then(() => $.get('nyan.txt')).then(nyan => results.nyan = nyan)
.then(() => console.log(results.hoge + results.piyo + results.nyan));
(async() => {
  const [hoge, piyo] = await Promise.all([
    $.get('hoge.txt'),
    $.get('piyo.txt')
  ]);
  const nyan = await $.get('nyan.txt');
  console.log(hoge + piyo + nyan);
})();

こちらも構成が似ていますね。そういう意味でasync/awaitは、既存実装をより簡略化したものとして認識できるため、好感が持てます。
async/await側ではES6で導入された分割代入まで使われており、新しい技術のオンパレードです。Promise側もアロー演算子をやめるとティロ・フィナーレですが。

Promise、アンタはいい子に育った。嘘もつかないし、悪いこともしない。

圧倒的と思われるasync/awaitのシンプルさですが、例外が絡んでくるとPromisecatchの流麗さも垣間見えます。

const results = {};
Promise.resolve()
  .then(() => $.get('hoge.txt')).then(hoge => results.hoge = hoge)
  .catch(e => results.hogeError = e)
  .then(() => $.get('piyo.txt')).then(piyo => results.piyo = piyo)
  .catch(e => results.piyoError = e)
  .then(() => console.log(JSON.stringify(results)));
(async() => {
  const results = {};
  try {
    results.hoge = await $.get('hoge.txt');
  } catch (e) {
    results.hogeError = e;
  }
  try {
    results.piyo = await $.get('piyo.txt');
  } catch (e) {
    results.piyoError = e;
  }
  console.log(JSON.stringify(results));
})();

次にPromiseasync/awaitで、オブジェクトメソッドとの連携を比較。

// オブジェクトを用意
const Capsule = {
  getHoge: function() {
    return $.get('hoge.txt').then(hoge => Capsule.hoge = hoge);
  },
  getPiyo: function() {
    return $.get('piyo.txt').then(piyo => Capsule.piyo = piyo);
  },
  out: function() {
    console.log(Capsule.hoge + Capsule.piyo);
  }
};
Promise.resolve().then(Capsule.getHoge).then(Capsule.getPiyo).then(Capsule.out);
(async() => {
  await Capsule.getHoge();
  await Capsule.getPiyo();
  Capsule.out();
})();

前回記事でも書きましたが、Promiseはオブジェクト指向と相性が良いです。組み方によってはPromiseにも軍配が上がります。
というよりasync/awaitの実装は不自然です。非同期っぽくないはずのasync/awaitが、かえって非同期感を煽っています。メイン関数(ファサード部)のawaitを謎の呪文(async() => {})();で囲う必要があるのも悲しいところ。

さやか、君がPromiseを覚えられない限り、杏子と戦っても、勝ち目は無いと思っていい。

こんなコードをよく見かけますよね。見かけないはずがない。

// データベースから取得する
function select(key, callback) {
  db.select(key, (err, result) => {
    if (err) {
      return callback(err);
    }
    return callback(null, result);
  });
}

// データベースから取得した値をクライアントに返す
select('key', (err, result) => {
  if (err) {
    res.send('ERR:' + err);
    return;
  }
  res.send('OK:' + result);
});

これをPromiseで書き換えると、こんな感じです。よくある光景です。

// データベースから取得する
function select(key) {
  return new Promise((resolve, reject) => {
    db.select(key, (err, result) => {
      if (err) {
        return reject(err);
      }
      return resolve(result);
    });
  });
}

// データベースから取得した値をクライアントに返す
Promise.resolve()
  .then(() => select('key'))
  .then(result => res.send('OK:' + result))
  .catch(err => res.send('ERR:' + err));

これをasync/awaitで書き換えると…

// データベースから取得する
function select() {
  // 書けない
}

// データベースから取得した値をクライアントに返す
(async() => {
  try {
    const result = await select('key');
    res.send('OK:' + result);
  } catch (err) {
    res.send('ERR:' + err);
  }
})();

Promiseには大きく分けて2種類あります。

  • 非同期処理の終了を検知する検知側
  • 非同期処理の処理順を記載する呼び出し側

呼び出し側はPromiseasync/awaitで書けますが、検知側はPromiseでしか書けません。さやかちゃん?起きて…ねぇ、ねぇちょっと、どうしたの?

何なんだよ、Observableって一体何なんだ!?さやかに何をしやがった!?

Promiseでは不可能なStream処理をやってくれるのがObservableです。Observableでも書いてみましょう。同期型のmapは難しいですが、今回はconcatMapを使います。

const result = [];
of([])
  .pipe(
    concatMap(() => $.get('hoge.txt')),
    map(v => result.push(v)),
    concatMap(() => $.get('piyo.txt')),
    map(v => result.push(v)),
    concatMap(() => $.get('nyan.txt')),
    map(v => result.push(v))
  )
  .subscribe(() => console.log(result));

Promiseの書き方とそっくりに書けました。
フロントサイドのAngularReactはズブズブとObservableに浸かっているので、たとえasync/awaitPromiseを撲滅しても、この書き方はまたやって来る。Java Stream APIからもやって来る。
第一級関数を忌み嫌う時、私達はグリーフシードになり、魔女として生まれ変わる。それが、魔法少女になった者の、逃れられない運命。

この街にはもう一人、async functionがいるからね

さやかちゃん、async functionは作れたって言ってるけど、でも、もしマミさんの時と同じようなことになったらって思うと。

async function hoge() {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve('hoge'), 1000);
  });
}
async function fuga() {
  const result = await hoge();
  console.log(result);
}
fuga(); // 1秒後に'hoge'が表示される

はぁ…まどか、fugaは本来のasync functionじゃなくて、ただの抜け殻なんだって。
hogeが本来のasync functionで、fugaawaitを使うだけためのasync functionなんだよ。
これを見てごらん。

async function hoge() {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve('hoge'), 1000);
  });
}
async function fuga() {
  const result = await hoge();
  console.log(result);
}
library.load(fuga).draw(); // draw()が即時実行されるか1秒待たされるか分からない

awaitを使いたくてfugaasync functionにしただけでも、library.loadがコールバックをawaitで待っていた場合、hogeの終了までブロックされるという予想外のことが起きることもあるんだ。こればっかりは、library.load実装によりけりだね。

もしブロックしたくないなら、こうやって安全策を取らなきゃいけない。

async function hoge() {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve('hoge'), 1000);
  });
}
function fuga() {
  (async() => {
    const result = await hoge();
    console.log(result);
  })();
}
library.load(fuga).draw(); // draw()は即時実行されて1秒後に'hoge'が表示される

さやかちゃんはさ、怖くはないの?後悔とか全然ないの?

美樹さやかが一生を費やして保守しても、バグが落ち着く日は来なかった。

実際の現場ではasync/awaitが脆すぎる可能性はあります。
コーディング規約をちゃんとしないと、こんなことになる可能性も…。

async function selectUpdate() {
  return update(await select());
}
selectUpdate()
  .then(o => res.send(o))
  .catch(e => res.send(e.message));

async関数がPromiseになる以上、thenスタイルが混ざり込むのはもはやご愛嬌。
恐いのはupdate関数の呼び出しにawaitがついてないのが、ミスかどうかすぐには分からないところ。今は大丈夫でもupdate関数の仕様が途中で変更される可能性があります。もしこれがバグの原因となり品質管理チームにバレてしまうと、「関数呼び出し時には必ずawaitをつける」というコーディング規約が爆誕し、多くの犠牲者が出るかもしれません。
そんなルールは有り得ないと言いたいところですが、悲しいことにPromisethenによる関数呼び出しは全て「awaitをつけて呼び出している状態」になっており、それはある意味で安全なのです。

Promise.allであえて非同期にしない限り、Promiseで書くと全て同期処理された。だけどasync/awaitにすると、awaitの付け方で思いもよらない非同期処理が生まれてしまう。私達魔法少女って、そう言う仕組みだったんだね。

あたしって、ホントばか。

おしまい