JavaScript Tips | 配列ユーティリティ:非同期 reduce

JavaScript JavaScript
スポンサーリンク

そもそも reduce と「非同期 reduce」の違い

Array.prototype.reduce は、配列を 1 つの値に“畳み込む”ための関数です。
合計を出したり、オブジェクトに集計したり、「最終的に 1 つの結果」を作るときに使います。

同期版 reduce の形はこうです。

array.reduce((acc, item, index) => {
  // acc に「これまでの結果」
  // item に「今の要素」
  // 新しい acc を返す
  return acc;
}, 初期値);
JavaScript

一方「非同期 reduce」は、この「acc を更新する処理」自体が async(Promise を返す)になっているバージョンです。
つまり「1 要素ごとに await しながら、順番に acc を育てていく」イメージになります。


基本形:順番に処理する asyncReduce(直列版)

実装と考え方

非同期 reduce の基本は「必ず順番に処理する」ことです。
reduce は「前の結果に基づいて次を計算する」ので、ここは並列にしづらいところです。

async function asyncReduce(array, asyncReducer, initialValue) {
  if (!Array.isArray(array)) {
    return initialValue;
  }
  if (typeof asyncReducer !== "function") {
    return initialValue;
  }

  let acc = initialValue;
  for (let i = 0; i < array.length; i++) {
    acc = await asyncReducer(acc, array[i], i);
  }
  return acc;
}
JavaScript

重要なポイントをかみ砕きます。

asyncReducer は「非同期な reducer 関数」です(async 関数 or Promise を返す関数)。
acc は「蓄積値」で、毎回 await asyncReducer(...) の結果で更新されます。
for ループで 0 → 1 → 2 → … と、必ず順番に処理されます。
戻り値は Promise なので、呼び出し側は await asyncReduce(...) で最終結果を受け取ります。

「同期 reduce を、そのまま async/await にしたもの」と考えるとイメージしやすいです。


例題 1:非同期 API の結果を合計する

シナリオ

「ID の配列があって、各 ID ごとに API を叩くと amount が返ってくる。
それを全部足し合わせて、合計金額を出したい」というよくある業務シナリオを考えます。

コード

async function fetchAmount(id) {
  // 実際には fetch などで API を叩く想定
  await new Promise((r) => setTimeout(r, 200));
  return { id, amount: id * 100 }; // 例: id=1 → 100, id=2 → 200
}

async function main() {
  const ids = [1, 2, 3];

  const total = await asyncReduce(
    ids,
    async (acc, id) => {
      const res = await fetchAmount(id);
      return acc + res.amount;
    },
    0
  );

  console.log(total); // 600
}

main();
JavaScript

ここでの流れを言葉で追うと、こうなります。

最初の acc は 0。
id=1 のとき、API を叩いて amount=100 を受け取り、acc は 100 になる。
次に id=2 で API を叩き、amount=200 を足して acc は 300。
次に id=3 で API を叩き、amount=300 を足して acc は 600。
最後に 600 が返ってくる。

「非同期処理を挟みながら、acc を順番に育てていく」という reduce の本質が、そのまま async になっています。


例題 2:非同期バリデーションを順番に積み上げる

シナリオ

「入力値に対して、複数のバリデーションを順番に実行したい。
各バリデーションはサーバーに問い合わせる必要があり、非同期。
エラーがあればエラーメッセージを配列に貯めていきたい。」というケースを考えます。

コード

async function validateLength(value) {
  await new Promise((r) => setTimeout(r, 100));
  return value.length >= 3 ? null : "3文字以上で入力してください";
}

async function validateUnique(value) {
  await new Promise((r) => setTimeout(r, 100));
  const used = ["foo", "bar"];
  return used.includes(value) ? "すでに使われている値です" : null;
}

async function main() {
  const value = "fo";

  const validators = [validateLength, validateUnique];

  const errors = await asyncReduce(
    validators,
    async (acc, validator) => {
      const message = await validator(value);
      if (message) {
        acc.push(message);
      }
      return acc;
    },
    []
  );

  console.log(errors);
  // ["3文字以上で入力してください"]
}

main();
JavaScript

ここでのポイントは、「acc が配列」であることです。

最初の acc は []
validateLength を実行して、エラーがあれば配列に push。
次に validateUnique を実行して、同じようにエラーがあれば push。
最後に「エラーメッセージの配列」が返ってくる。

このように、「非同期な処理を順番に適用しながら、結果を 1 つにまとめる」場面で asyncReduce はとても相性が良いです。


例題 3:非同期処理を使った「段階的な状態更新」

シナリオ

「初期状態のオブジェクトがあり、
それに対して“非同期な更新ステップ”を順番に適用していきたい」というケースを考えます。

例えば、

ステップ 1:API からユーザー情報を取得して state に載せる。
ステップ 2:別の API から権限情報を取得して state に載せる。
ステップ 3:ログイン履歴を取得して state に載せる。

というような「状態を少しずつ育てていく」処理です。

コード

async function loadUser(state) {
  await new Promise((r) => setTimeout(r, 100));
  return { ...state, user: { id: 1, name: "Alice" } };
}

async function loadPermissions(state) {
  await new Promise((r) => setTimeout(r, 100));
  return { ...state, permissions: ["read", "write"] };
}

async function loadHistory(state) {
  await new Promise((r) => setTimeout(r, 100));
  return { ...state, history: ["login-1", "login-2"] };
}

async function main() {
  const steps = [loadUser, loadPermissions, loadHistory];

  const finalState = await asyncReduce(
    steps,
    async (state, step) => {
      return step(state);
    },
    {} // 初期 state
  );

  console.log(finalState);
  /*
  {
    user: { id: 1, name: "Alice" },
    permissions: ["read", "write"],
    history: ["login-1", "login-2"]
  }
  */
}

main();
JavaScript

ここでは、「acc = state」として扱っています。

初期 state は {}
loadUser で user を載せた state が返ってくる。
その state を loadPermissions に渡して、さらに permissions を載せる。
最後に loadHistory で history を載せて、完成した state が返ってくる。

「状態を段階的に育てる」処理と reduce は非常に相性が良く、それが非同期でも同じ構造で書けます。


非同期 reduce を使うときの重要な意識ポイント

acc(蓄積値)が「何なのか」を最初に決める

非同期 reduce でも、いちばん大事なのは「acc が何を表しているか」をはっきりさせることです。

合計なら acc は数値。
エラー一覧なら acc は配列。
状態なら acc はオブジェクト。

そして、「1 要素処理するたびに acc をどう変化させたいか」を日本語で説明できるようにしてからコードに落とすと、
asyncReduce の中身がスッと書けるようになります。

非同期 reduce は基本的に「直列」である

reduce は「前の結果に依存して次を計算する」構造なので、
基本的には「順番に処理する(直列)」のが自然です。

もし「前の結果に依存しない」のであれば、
reduce ではなく asyncMapPromise.all で並列に処理したほうがシンプルです。

「これは本当に reduce なのか? それとも map のほうが合っているのか?」を一度立ち止まって考えると、設計がきれいになります。

戻り値は必ず Promise になる

asyncReduceasync function なので、戻り値は必ず Promise です。

const p = asyncReduce(...);   // これは Promise
const result = await asyncReduce(...); // これで中身(最終結果)が取れる
JavaScript

ここを忘れて「result が配列(や数値)だと思っていたら Promise だった」というのは、非同期あるあるの典型です。


手を動かして非同期 reduce の感覚をつかむ

次のコードをそのまま実行して、ログの出方と acc の変化を眺めてみてください。

async function asyncReduce(array, asyncReducer, initialValue) {
  if (!Array.isArray(array)) return initialValue;
  if (typeof asyncReducer !== "function") return initialValue;

  let acc = initialValue;
  for (let i = 0; i < array.length; i++) {
    console.log("before step", i, "acc =", acc);
    acc = await asyncReducer(acc, array[i], i);
    console.log("after  step", i, "acc =", acc);
  }
  return acc;
}

async function demo() {
  const ids = [1, 2, 3];

  const total = await asyncReduce(
    ids,
    async (acc, id) => {
      await new Promise((r) => setTimeout(r, 200));
      return acc + id;
    },
    0
  );

  console.log("final total =", total);
}

demo();
JavaScript

「ステップごとに acc がどう変わっていくか」をログで追うと、
非同期 reduce の“動き”がかなりクリアに見えてくるはずです。


まとめ:非同期 reduce ユーティリティで「配列 × async × 集約」を標準化する

業務コードでは、「配列の要素を順番に非同期処理しながら、1 つの結果にまとめる」場面がよく出てきます。
そのたびに for ループと await をベタ書きするのではなく、

export async function asyncReduce(array, asyncReducer, initialValue) { ... }
JavaScript

のようなユーティリティを 1 つ用意しておく。
そして、「配列 × async × 集約」が必要になったら、まず asyncReduce を使う。

そう決めておくと、

「これは何をしているコードか?」が関数名で伝わる。
「順番に処理している」ことが構造として保証される。
acc の型と初期値を意識する習慣がつき、設計が安定する。

という、かなり“業務レベル”な書き味に近づいていきます。

タイトルとURLをコピーしました