JavaScript Tips | 配列ユーティリティ:フィルタ合成

JavaScript JavaScript
スポンサーリンク

何をしたいユーティリティか:「フィルタ合成」

「フィルタ合成」は、複数の条件(フィルタ)を組み合わせて、1つのフィルタ関数として扱えるようにするテクニックです。
もう少しくだいて言うと、「小さな条件関数をいくつか作っておいて、それらを“AND や OR でつないだ 1 個のフィルタ”として再利用できるようにする」ものです。

業務では、配列に対して filter を何度も書くことがよくあります。
そのたびに item.active && item.role === "admin" && item.age >= 20 のような長い条件を書くと、だんだん読めなくなります。
そこで、「条件を小さく分けて」「それを合成して」「filter に渡す」というスタイルにすると、コードがかなりスッキリします。


前提確認:filter は「条件を満たす要素だけ残す」

まず、Array.prototype.filter の基本を軽くおさらいします。

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

const isEven = (n) => n % 2 === 0;

const evens = numbers.filter(isEven);
// [2, 4]
JavaScript

filter は、「true を返した要素だけ残す」関数です。
ここでは isEven がフィルタ関数で、「偶数なら true、奇数なら false」を返しています。

フィルタ合成とは、この isEven のような「条件関数」を複数組み合わせて、
「複雑な条件を 1 個のフィルタ関数として扱えるようにする」ことです。


フィルタ合成の基本アイデア

フィルタ合成の基本はとてもシンプルです。

小さな条件関数をいくつか用意する。
それらを AND(全部満たす)や OR(どれか満たす)でまとめる関数を作る。
その「まとめた関数」を filter に渡す。

これをユーティリティとして切り出しておくと、
「条件の組み合わせを変えるだけで、いろいろなフィルタが簡単に作れる」ようになります。


AND でフィルタを合成する combineAll(すべて満たす)

実装例:すべてのフィルタを満たす合成フィルタ

まずは、「全部の条件を満たしたものだけ残す」AND 合成です。

function combineAll(predicates) {
  return function combined(item, index) {
    if (!Array.isArray(predicates) || predicates.length === 0) {
      return true;
    }
    return predicates.every((p) => typeof p === "function" && p(item, index));
  };
}
JavaScript

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

predicates は「条件関数の配列」です。
combineAll は「その配列を受け取って、新しいフィルタ関数 combined を返す関数」です。
combined が呼ばれたとき、predicates.every(...) で「全部の条件関数が true を返すか」をチェックします。
1つでも false になったら、その要素はフィルタで落ちます。

つまり、「複数条件の AND(かつ)」を 1 個のフィルタ関数にまとめているわけです。

例題:active かつ role=admin のユーザーだけ残す

const users = [
  { id: 1, name: "A", active: true,  role: "user" },
  { id: 2, name: "B", active: false, role: "admin" },
  { id: 3, name: "C", active: true,  role: "admin" },
];

const isActive = (u) => u.active === true;
const isAdmin  = (u) => u.role === "admin";

const filterActiveAdmin = combineAll([isActive, isAdmin]);

const result = users.filter(filterActiveAdmin);

/*
[
  { id: 3, name: "C", active: true, role: "admin" }
]
*/
JavaScript

ここで大事なのは、「filter に渡しているのは 1 個の関数 filterActiveAdmin だけ」ということです。
その中で、isActiveisAdmin の 2 つの条件が AND で合成されています。


OR でフィルタを合成する combineAny(どれか満たす)

実装例:どれか1つでも条件を満たせば残す

次は、「複数条件のうち、どれか1つでも満たせば残す」OR 合成です。

function combineAny(predicates) {
  return function combined(item, index) {
    if (!Array.isArray(predicates) || predicates.length === 0) {
      return true;
    }
    return predicates.some((p) => typeof p === "function" && p(item, index));
  };
}
JavaScript

some は「1つでも true があれば true」を返す関数です。
つまり、「複数条件の OR(または)」を 1 個のフィルタ関数にまとめています。

例題:admin か editor のユーザーだけ残す

const users = [
  { id: 1, role: "user" },
  { id: 2, role: "admin" },
  { id: 3, role: "editor" },
  { id: 4, role: "guest" },
];

const isAdmin  = (u) => u.role === "admin";
const isEditor = (u) => u.role === "editor";

const filterPrivileged = combineAny([isAdmin, isEditor]);

const result = users.filter(filterPrivileged);

/*
[
  { id: 2, role: "admin" },
  { id: 3, role: "editor" },
]
*/
JavaScript

「admin または editor」という OR 条件を、
combineAny([isAdmin, isEditor]) という形で表現できています。


否定を合成する notFilter(条件の反転)

「この条件を満たさないものだけ残したい」

ときどき、「この条件に当てはまるものを除外したい」というケースがあります。
そのときに便利なのが、「フィルタを反転する」ユーティリティです。

function notFilter(predicate) {
  return function negated(item, index) {
    if (typeof predicate !== "function") {
      return true;
    }
    return !predicate(item, index);
  };
}
JavaScript

predicate が true を返したら false に、false を返したら true にひっくり返します。

例題:active ではないユーザーだけ残す

const users = [
  { id: 1, active: true },
  { id: 2, active: false },
  { id: 3, active: false },
];

const isActive = (u) => u.active === true;
const isInactive = notFilter(isActive);

const result = users.filter(isInactive);

/*
[
  { id: 2, active: false },
  { id: 3, active: false },
]
*/
JavaScript

「active ではない」という条件を、
notFilter(isActive) という形で表現できています。


フィルタ合成を組み合わせた実務的な例

例題:複雑な条件を読みやすく書く

例えば、次のような要件を考えます。

active である。
role が admin または editor である。
年齢が 20 歳以上である。

これをベタ書きすると、こうなります。

const result = users.filter(
  (u) =>
    u.active &&
    (u.role === "admin" || u.role === "editor") &&
    u.age >= 20
);
JavaScript

一行で書けますが、条件が増えるとどんどん読みにくくなります。
フィルタ合成を使うと、次のように分解できます。

const isActive = (u) => u.active;
const isAdmin  = (u) => u.role === "admin";
const isEditor = (u) => u.role === "editor";
const isAdult  = (u) => u.age >= 20;

const isPrivileged = combineAny([isAdmin, isEditor]);
const filterUser   = combineAll([isActive, isPrivileged, isAdult]);

const result = users.filter(filterUser);
JavaScript

ここでのポイントは、「条件の意味が関数名で読める」ことです。
combineAllcombineAny を使うことで、「これは AND 条件」「これは OR 条件」という意図も明確になります。


フィルタ合成のメリットを整理する

フィルタ合成には、実務的にかなり大きなメリットがあります。

条件を小さな関数に分けることで、テストしやすくなる。
条件の組み合わせを変えるだけで、別のフィルタを簡単に作れる。
filter の中に長い論理式を書かなくて済むので、読みやすくなる。
AND / OR / NOT の意図が、ユーティリティ名で一目で分かる。

特に大規模な業務コードでは、「条件が増える」「仕様が変わる」が日常茶飯事です。
フィルタ合成を使っておくと、「この条件を追加したい」「この条件を外したい」といった変更に強くなります。


手を動かしてフィルタ合成の感覚をつかむ

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

function combineAll(predicates) {
  return function combined(item, index) {
    if (!Array.isArray(predicates) || predicates.length === 0) return true;
    return predicates.every((p) => typeof p === "function" && p(item, index));
  };
}

function combineAny(predicates) {
  return function combined(item, index) {
    if (!Array.isArray(predicates) || predicates.length === 0) return true;
    return predicates.some((p) => typeof p === "function" && p(item, index));
  };
}

function notFilter(predicate) {
  return function negated(item, index) {
    if (typeof predicate !== "function") return true;
    return !predicate(item, index);
  };
}

const users = [
  { id: 1, active: true,  role: "user",  age: 18 },
  { id: 2, active: true,  role: "admin", age: 25 },
  { id: 3, active: false, role: "admin", age: 30 },
  { id: 4, active: true,  role: "editor", age: 22 },
];

const isActive = (u) => u.active;
const isAdmin  = (u) => u.role === "admin";
const isEditor = (u) => u.role === "editor";
const isAdult  = (u) => u.age >= 20;

const isPrivileged = combineAny([isAdmin, isEditor]);
const filterUser   = combineAll([isActive, isPrivileged, isAdult]);

console.log(users.filter(filterUser));
JavaScript

どのユーザーがヒットするか、combineAllcombineAny に変えたらどう変わるか、
notFilter を混ぜたらどうなるか、いろいろ試してみると感覚がつかめます。


まとめ:フィルタ合成は「条件ロジックを整理するための武器」

フィルタ合成は、単なるテクニックではなく、「条件ロジックを整理するための設計パターン」です。

プロジェクトに次のようなユーティリティを置いておくイメージです。

export function combineAll(predicates) { ... }   // AND 合成
export function combineAny(predicates) { ... }   // OR 合成
export function notFilter(predicate) { ... }     // 否定
JavaScript

そして、「複雑な filter を書きたくなったら、まず条件を小さな関数に分けて、合成してから使う」と決めておく。
それだけで、配列フィルタのコードが一段読みやすく、変更に強く、テストしやすくなります。

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