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

JavaScript JavaScript
スポンサーリンク

何をしたいユーティリティか:「非同期 filter」

「非同期 filter」は、配列の各要素に対して「async な条件チェック」を行い、条件を満たした要素だけを残すユーティリティです。
普通の Array.prototype.filter は同期処理専用なので、次のような場面ではそのまま使えません。

API を叩いて「有効なユーザーかどうか」を判定して、OK なユーザーだけ残したい。
DB に問い合わせて「権限があるかどうか」を確認して、権限ありのレコードだけ残したい。

こういう「条件判定そのものが非同期」のときに必要になるのが、非同期版 filter(async filter)です。


前提確認:同期 filter の基本イメージ

まずは、普通の filter が何をしているかをもう一度整理します。

const numbers = [1, 2, 3, 4, 5];

const evens = numbers.filter((n) => n % 2 === 0);
// [2, 4]
JavaScript

filter は、「コールバックが true を返した要素だけ残す」関数です。
ここでは n % 2 === 0 が「偶数なら true、奇数なら false」を返す条件関数になっています。

重要なのは、この条件関数は同期であることが前提だという点です。
async (n) => { ... } のような非同期関数を渡しても、filter はそれを待ってくれません。


基本形:全部並列で判定する asyncFilter(Promise.all を使う)

実装と考え方

まずは、「全部並列で条件判定をして、true になったものだけ残す」一番シンプルな形です。

async function asyncFilter(array, asyncPredicate) {
  if (!Array.isArray(array)) {
    return [];
  }
  if (typeof asyncPredicate !== "function") {
    return array.slice();
  }

  const promises = array.map((item, index) => asyncPredicate(item, index));
  const results = await Promise.all(promises);

  const filtered = [];
  for (let i = 0; i < array.length; i++) {
    if (results[i]) {
      filtered.push(array[i]);
    }
  }

  return filtered;
}
JavaScript

ここでの重要ポイントをかみ砕きます。

asyncPredicate は「非同期な条件関数」(async 関数 or Promise を返す関数)。
まず普通の map で「条件判定の Promise の配列」を作る。
Promise.all で「全部の判定が終わるのを待って、結果(true/false の配列)を受け取る」。
その結果配列を見ながら、元の配列から「true の位置の要素だけ」を取り出して新しい配列を作る。

つまり、「同期 filter の非同期版」を素直に書いた形です。

例題:API で「有効ユーザーかどうか」を判定して、OK なユーザーだけ残す

async function isValidUser(userId) {
  // 実際には fetch などで API を叩く想定
  // ここでは簡易的に「偶数 ID だけ有効」とする
  await new Promise((r) => setTimeout(r, 200));
  return userId % 2 === 0;
}

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

  const validIds = await asyncFilter(ids, (id) => isValidUser(id));

  console.log(validIds);
  // [2, 4]
}

main();
JavaScript

「ID ごとに API を叩いて有効かどうか判定し、有効な ID だけ残す」という処理が、
asyncFilter でかなり素直に書けています。


順番に判定する asyncFilterSeries(直列版)

なぜ直列版が必要になるのか

全部並列で判定すると速いですが、業務ではこういう制約もよくあります。

API のレート制限が厳しいので、一気に叩きたくない。
DB やファイルへのアクセスを順番にやりたい。

その場合は、「1 件ずつ await してから次へ進む」直列版の async filter が欲しくなります。

実装

async function asyncFilterSeries(array, asyncPredicate) {
  if (!Array.isArray(array)) {
    return [];
  }
  if (typeof asyncPredicate !== "function") {
    return array.slice();
  }

  const filtered = [];
  for (let i = 0; i < array.length; i++) {
    const item = array[i];
    const ok = await asyncPredicate(item, i);
    if (ok) {
      filtered.push(item);
    }
  }

  return filtered;
}
JavaScript

ここでは for ループを使って、1 要素ごとに await してから次へ進むようにしています。
これで、「必ず順番に判定される非同期 filter」が手に入ります。

例題:ログを順番に外部サービスに問い合わせて、「重要かどうか」を判定する

async function isImportantLog(log) {
  // 実際には外部サービスに問い合わせる想定
  console.log("check:", log.message);
  await new Promise((r) => setTimeout(r, 200));
  return log.level === "error" || log.level === "warning";
}

async function main() {
  const logs = [
    { level: "info",    message: "OK" },
    { level: "warning", message: "Slow" },
    { level: "error",   message: "Failed" },
  ];

  const important = await asyncFilterSeries(logs, (log) => isImportantLog(log));

  console.log(important);
  /*
  [
    { level: "warning", message: "Slow" },
    { level: "error",   message: "Failed" },
  ]
  */
}

main();
JavaScript

asyncFilterSeries を使うことで、「ログを 1 件ずつ順番にチェックする」という流れが保証されます。


同時実行数を制限する asyncFilterLimit(負荷とレート制限に強い形)

「全部並列は怖い、でも完全直列は遅い」をどう解決するか

API を 1000 件分叩いて判定するようなケースで、全部並列は危険です。
一方で、完全に 1 件ずつ直列でやると時間がかかりすぎることもあります。

そこで、「同時に動かす判定の数を制限する」非同期 filter があると便利です。

実装イメージ

async function asyncFilterLimit(array, asyncPredicate, limit = 5) {
  if (!Array.isArray(array)) {
    return [];
  }
  if (typeof asyncPredicate !== "function") {
    return array.slice();
  }
  if (typeof limit !== "number" || limit <= 0) {
    limit = 1;
  }

  const results = new Array(array.length);
  let currentIndex = 0;

  async function worker() {
    while (currentIndex < array.length) {
      const index = currentIndex;
      currentIndex += 1;

      const item = array[index];
      const ok = await asyncPredicate(item, index);
      results[index] = ok;
    }
  }

  const workers = [];
  const workerCount = Math.min(limit, array.length);
  for (let i = 0; i < workerCount; i++) {
    workers.push(worker());
  }

  await Promise.all(workers);

  const filtered = [];
  for (let i = 0; i < array.length; i++) {
    if (results[i]) {
      filtered.push(array[i]);
    }
  }

  return filtered;
}
JavaScript

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

limit は「同時に動かす最大数」。
worker という「仕事を取りに行く非同期関数」を複数立ち上げる。
currentIndex を共有して、「まだ判定していないインデックス」を取り合う。
各 worker は「仕事がなくなるまでループ」して判定する。
最後に results を見て、true の位置の要素だけを集める。

これで、「最大 limit 個まで並列で動く非同期 filter」が実現できます。

例題:ユーザー ID を最大 3 並列で判定する

async function isValidUser(id) {
  console.log("check start", id);
  await new Promise((r) => setTimeout(r, 300));
  console.log("check end", id);
  return id % 2 === 0;
}

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

  const validIds = await asyncFilterLimit(ids, (id) => isValidUser(id), 3);

  console.log(validIds);
  // [2, 4, 6]
}

main();
JavaScript

ログを見てみると、「常に最大 3 件まで同時に判定している」ことが分かるはずです。
これが、レート制限や負荷を意識した“業務レベルの非同期 filter”です。


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

戻り値は必ず Promise になる

asyncFilter / asyncFilterSeries / asyncFilterLimit は、どれも async function です。
つまり、戻り値は必ず Promise になります。

使う側は必ず await するか、.then(...) で受け取る必要があります。

const p = asyncFilter(ids, isValidUser);   // これは Promise
const validIds = await asyncFilter(ids, isValidUser); // これで配列になる
JavaScript

ここを忘れると、「配列だと思っていたら Promise だった」という典型的なハマり方をします。

条件関数の戻り値は「真偽値(またはそれに変換できるもの)」にする

asyncPredicate は、「その要素を残すかどうか」を決める関数です。
戻り値は基本的に true/false にしておくと、読みやすくなります。

数値や文字列を返しても JavaScript 的には真偽値に変換されますが、
業務コードでは「この関数は true/false を返す」と決めておいたほうが、意図が伝わりやすいです。

エラーをどう扱うかを決める

非同期判定の中でエラーが起きたときに、

その要素を「落とす(false とみなす)」のか。
全体を「失敗(reject)」とみなすのか。

を、ユーティリティのルールとして決めておくとよいです。

例えば、「エラーならその要素は無効として落とす」なら、こう書けます。

async function safePredicate(item) {
  try {
    return await asyncPredicate(item);
  } catch (e) {
    return false;
  }
}
JavaScript

このように、「エラーをどう扱うか」も含めて非同期 filter の設計になります。


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

次のコードをコンソール(Node など)で実行して、挙動を自分の目で確認してみてください。

async function asyncFilter(array, asyncPredicate) {
  if (!Array.isArray(array)) return [];
  if (typeof asyncPredicate !== "function") return array.slice();

  const promises = array.map((item, index) => asyncPredicate(item, index));
  const results = await Promise.all(promises);

  const filtered = [];
  for (let i = 0; i < array.length; i++) {
    if (results[i]) {
      filtered.push(array[i]);
    }
  }
  return filtered;
}

async function asyncFilterSeries(array, asyncPredicate) {
  if (!Array.isArray(array)) return [];
  if (typeof asyncPredicate !== "function") return array.slice();

  const filtered = [];
  for (let i = 0; i < array.length; i++) {
    const ok = await asyncPredicate(array[i], i);
    if (ok) {
      filtered.push(array[i]);
    }
  }
  return filtered;
}

async function asyncFilterLimit(array, asyncPredicate, limit = 2) {
  if (!Array.isArray(array)) return [];
  if (typeof asyncPredicate !== "function") return array.slice();
  if (typeof limit !== "number" || limit <= 0) limit = 1;

  const results = new Array(array.length);
  let currentIndex = 0;

  async function worker() {
    while (currentIndex < array.length) {
      const index = currentIndex;
      currentIndex += 1;
      const ok = await asyncPredicate(array[index], index);
      results[index] = ok;
    }
  }

  const workers = [];
  const workerCount = Math.min(limit, array.length);
  for (let i = 0; i < workerCount; i++) {
    workers.push(worker());
  }

  await Promise.all(workers);

  const filtered = [];
  for (let i = 0; i < array.length; i++) {
    if (results[i]) {
      filtered.push(array[i]);
    }
  }
  return filtered;
}

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

  console.log("=== asyncFilter (parallel) ===");
  console.log(
    await asyncFilter(ids, async (id) => {
      console.log("check", id);
      await new Promise((r) => setTimeout(r, 300));
      return id % 2 === 0;
    })
  );

  console.log("=== asyncFilterSeries (series) ===");
  console.log(
    await asyncFilterSeries(ids, async (id) => {
      console.log("check", id);
      await new Promise((r) => setTimeout(r, 300));
      return id % 2 === 0;
    })
  );

  console.log("=== asyncFilterLimit (limit=2) ===");
  console.log(
    await asyncFilterLimit(ids, async (id) => {
      console.log("check", id);
      await new Promise((r) => setTimeout(r, 300));
      return id % 2 === 0;
    }, 2)
  );
}

demo();
JavaScript

並列・直列・同時実行数制限の違いが、ログの出方で体感できるはずです。


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

業務コードでは、「配列の各要素に対して非同期な条件判定をして、OK なものだけ残す」場面が本当に多いです。
そのたびに Promise.all や for ループをベタ書きするのではなく、

export async function asyncFilter(...) { ... }        // 全並列
export async function asyncFilterSeries(...) { ... }  // 直列
export async function asyncFilterLimit(...) { ... }   // 同時実行数制限
JavaScript

のようなユーティリティを用意して、「配列 × async 条件は必ずこれを通す」と決めておくと、
コードの意図が一気に読みやすくなります。

「速さを優先するのか」「順番を守るのか」「負荷を抑えるのか」――
その選択を関数名に埋め込んでおくことが、非同期処理を“業務レベル”で扱ううえでの大事な設計になります。

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