何をしたいユーティリティか:「フィルタ合成」
「フィルタ合成」は、複数の条件(フィルタ)を組み合わせて、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]
JavaScriptfilter は、「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 だけ」ということです。
その中で、isActive と isAdmin の 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));
};
}
JavaScriptsome は「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);
};
}
JavaScriptpredicate が 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ここでのポイントは、「条件の意味が関数名で読める」ことです。combineAll と combineAny を使うことで、「これは 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どのユーザーがヒットするか、combineAll を combineAny に変えたらどう変わるか、notFilter を混ぜたらどうなるか、いろいろ試してみると感覚がつかめます。
まとめ:フィルタ合成は「条件ロジックを整理するための武器」
フィルタ合成は、単なるテクニックではなく、「条件ロジックを整理するための設計パターン」です。
プロジェクトに次のようなユーティリティを置いておくイメージです。
export function combineAll(predicates) { ... } // AND 合成
export function combineAny(predicates) { ... } // OR 合成
export function notFilter(predicate) { ... } // 否定
JavaScriptそして、「複雑な filter を書きたくなったら、まず条件を小さな関数に分けて、合成してから使う」と決めておく。
それだけで、配列フィルタのコードが一段読みやすく、変更に強く、テストしやすくなります。
