銀の弾丸

プログラミングに関して、いろいろ書き残していければと思っております。

非同期処理の直列化:今やArray.reduceを使わなくてもできますよね

f:id:takamints:20181114220424j:plain
photo credit: hans-johnson 700-7000 Series_1 via photopin (license)

非同期処理の直列化とは「複数の非同期処理を、順番に実行する処理」のことです。非同期処理の順次実行や逐次実行とも呼ばれます。 処理速度は、並列処理よりも遅いのですが、処理順が重要であったり、先に実行した非同期処理の結果を次の非同期処理に使用する場合に必要になります。

JavaScriptでの非同期処理の直列化のやり方を検索すると「Array.reduce を使えばできる」とよく出てきます。

しかしアレ、そんなに便利ですかね?

コードが直感的じゃないし、ぱっと見ナニやってるのか分かりにくい。処理効率もそれほど良くなさそうです。

なんかもう「他のやり方ではできないよ」って誤解しそうな勢いですが、そうじゃない。 ES2017(ES8)以降のJavaScriptならもっとシンプルに書けますよね。

確かに「目的外使用」的で、軽く目からうろこが落ちて、「なるほどイイね!」と思う気持ちは理解できるのですが、それよか「誰にとっても直感的でわかりやすいコードを書く」ほうが重要ではないかと思ってます。

実際MDNでも、 reduceを使った直列化のコード が紹介されています。でも「こうすればできる」は「こうするべき」ではないですよね。

Vue.js入門 基礎から実践アプリケーション開発まで
川口 和也 喜多 啓介 野田 陽平 手島 拓也 片山 真也
技術評論社
売り上げランキング: 1,741

reduce使わずどうするか

じゃあreduce使わずにどうするの?っていうと、単純にfor ループで ES2017(ES8) の async / await を使えば普通に書けますfor~of 文ならもっとラク。そして reduce よりも明らかに簡潔です。

async/awaitが使えないならreduceを使うしかなさそうですが、そんなレガシーな環境で動かしたいなら Babel で変換すればよろしいのです。

※ ちなみに、Array.forEachでループを回すと並列処理になってしまいますので使えません。

for~of で非同期処理を直列化するコード

以下、ちょっと長くなっていますが、MDNのreduceのページで紹介されているreduceを使った直列化のコードfor~of を使った同等の処理を追加しました。

/**
 * for~of を使ってPromise配列をチェインしながら実行する。
 * (下のreduceバージョンと比べて如何に簡潔か確認)
 *
 * @async
 * @param {array} arr - Promise配列
 * @return {Object} Promiseオブジェクトを返す
 */
async function runPromiseInSequenseByForOf(arr) {
  let res;
  for(const currentPromise of arr) {
      res = await currentPromise(res);
  }
  return res;
}

/**
 * reduce を使ってPromise配列をチェインしながら実行する。
 *
 * @param {array} arr - Promise配列
 * @return {Object} Promiseオブジェクトを返す
 */
function runPromiseInSequense(arr) {
  return arr.reduce((promiseChain, currentPromise) => {
    return promiseChain.then((chainedResult) => {
      return currentPromise(chainedResult)
        .then((res) => res)
    })
  }, Promise.resolve());
}

// promise function 1
function p1() {
  return new Promise((resolve, reject) => {
    resolve(5);
  });
}

// promise function 2
function p2(a) {
  return new Promise((resolve, reject) => {
    resolve(a * 2);
  });
}

// promise function 3
function p3(a) {
  return new Promise((resolve, reject) => {
    resolve(a * 3);
  });
}

const promiseArr = [p1, p2, p3];

// Reduceバージョンで直列化
runPromiseInSequense(promiseArr)
  .then((res) => {
    console.log(res);   // 30
  });

// For~of バージョンで直列化
runPromiseInSequenseByForOf(promiseArr)
  .then((res) => {
    console.log(res);   // 30(結果は同じ)
  });

これ見て、どっちが良いかって考えると、どう考えても for~of のほうが良いと思うんですよね。

ただ、関数型プログラミングに傾倒している人にとってはletで宣言した変数resを逐次更新するところが気持ち悪いのかもしれません。