LoginSignup
23
14

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

Last updated at Posted at 2018-12-13

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は、ここではかえって非同期感を煽っています。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/awaitへの書き換えはできない
}

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

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

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

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

何なんだよ、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を撲滅しても、この書き方はまたやって来る。
第一級関数を忌み嫌う時、私達はグリーフシードになり、魔女として生まれ変わる。それが、魔法少女になった者の、逃れられない運命。

この街にはもう一人、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秒待たされるか分からない

awaithogeを待ちたいという理由だけでfugaasync functionにしたとしても、library.loadがコールバックをawaitで待っていた場合、drawhogeの終了までブロックされるという挙動に変更されるんだ。こればっかりは、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));

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

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

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

おしまい

23
14
5

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
23
14