はじめに
JavaScript/TypeScriptで関数をより柔軟に再利用するためのテクニックとして「カリー化」と「関数合成」があります。
この記事では、これらの概念を実務的なコードとともに解説し、特にフィルター処理にどう活かせるかを紹介します。
カリー化(Currying)とは?
カリー化とは、複数の引数をとる関数を、1引数ずつの関数に分解することです。
// 通常の関数
const add = (a: number, b: number): number => a + b;
// カリー化された関数
const curryAdd = (a: number) => (b: number): number => a + b;
const add3 = curryAdd(3);
console.log(add3(5)); // 8
この curryAdd
の挙動を丁寧に説明します:
-
curryAdd(3)
を呼び出すと、b: number => 3 + b
という関数が返ります。 - 返ってきた関数に
5
を渡すと、3 + 5
となり8
を返します。 - つまり、引数を一つずつ渡していくイメージです。
メリット
- 引数の一部だけ渡して再利用できる
- 高階関数としての柔軟性が上がる
関数合成(Function Composition)とは?
関数合成とは、複数の関数をつなげて1つの処理にすることです。
const trim = (s: string) => s.trim();
const toLower = (s: string) => s.toLowerCase();
const compose = <T>(f: (x: T) => T, g: (x: T) => T) => (x: T) => f(g(x));
この compose
関数の動きを具体的に説明します:
-
compose(f, g)
は、新しい関数を返します。 - この新しい関数は引数
x
を受け取ると、まずg(x)
を実行し、次にその結果をf
に渡します。 - つまり、
f(g(x))
のように順番に適用されるのです。
使用例:
const sanitize = compose(toLower, trim);
console.log(sanitize(" HeLLo ")); // "hello"
この場合、" HeLLo "
がまず trim
により余分なスペースを除去され、次に toLower
により小文字化されます。
カリー化を使った柔軟なフィルター処理の実装
ユーザーデータとその型定義
まず、フィルタリングするデータの型を明示的に定義しましょう。
// ユーザー型の定義
type User = {
userId: number;
userName: string;
email: string;
password: string;
dateOfBirth: Date;
countryCode: number;
type: string;
};
// Controllerから渡ってくるデータ(Userの配列)
const users: User[] = [
{
userId: 1,
userName: "tommy434",
email: "tommy434@example.com",
password: "c387c2fb3e99cb5",
dateOfBirth: new Date("1982-05-25"),
countryCode: 1,
type: "admin",
},
{
userId: 2,
userName: "doorpink9",
email: "doors2doors@example.com",
password: "386449c7dba62e",
dateOfBirth: new Date("1979-02-16"),
countryCode: 1,
type: "default",
},
{
userId: 3,
userName: "parkriderz",
email: "parkriderz@example.com",
password: "79879ce55ebf7e0",
dateOfBirth: new Date("1990-10-18"),
countryCode: 1,
type: "subscriber",
},
{
userId: 4,
userName: "derkknight",
email: "derkknight93@example.com",
password: "9f340036bcb891",
dateOfBirth: new Date("1993-08-20"),
countryCode: 44,
type: "editor",
},
];
カリー化した関数を使って条件を作る
ユーザーをフィルターするための条件関数を、カリー化を使って実装します。
// ユーザータイプでフィルタリングする関数
const hasType = (type: string) => (user: User): boolean => user.type === type;
// 国コードでフィルタリングする関数
const fromCountry = (code: number) => (user: User): boolean => user.countryCode === code;
// 指定した年以前に生まれた人でフィルタリングする関数
const bornBefore = (year: number) => (user: User): boolean => user.dateOfBirth.getFullYear() <= year;
これらの関数は、条件をカリー化で部分適用できる関数です。各関数は引数を1つ受け取り、User型を引数に取って真偽値を返す関数(述語関数)を返します。
条件を合成する関数を定義
複数の条件をAND条件(すべての条件を満たす)で合成する関数を定義します。
// 複数の述語関数を受け取り、それらをAND条件で合成する関数
const pipePredicate = (predicates: Array<(user: User) => boolean>) =>
(user: User): boolean => predicates.every((fn) => fn(user));
使用例:条件を合成してフィルターする
定義した条件関数を組み合わせてフィルタリングしてみましょう。
// 複数条件でフィルタリング
const filtered = users.filter(
pipePredicate([
hasType("subscriber"), // タイプが "subscriber" のユーザー
fromCountry(1), // 国コードが 1 のユーザー
bornBefore(1995), // 1995年以前に生まれたユーザー
])
);
console.log(filtered);
この例では、次の条件に一致するユーザーだけが返されます:
- type が "subscriber"
- countryCode が 1
- dateOfBirth が1995年以前
このように、条件関数を柔軟に組み合わせて、読みやすく再利用可能なフィルター処理が可能になります。カリー化を利用することで、条件の一部だけを事前に適用した新しい関数を作ることができ、より柔軟なフィルター処理が実現できます。
実践的な例:複雑なフィルタリングシステムの構築
上記で紹介した技術を使って、より実践的なフィルタリングシステムを構築してみましょう。例えば、ECサイトで商品を検索するケースを考えてみます。
商品データの例
type Product = {
id: number;
name: string;
price: number;
category: string;
tags: string[];
inStock: boolean;
rating: number;
releaseDate: Date;
};
const products: Product[] = [
{
id: 1,
name: "高性能ノートPC",
price: 120000,
category: "electronics",
tags: ["laptop", "business", "windows"],
inStock: true,
rating: 4.5,
releaseDate: new Date("2023-05-15")
},
{
id: 2,
name: "ゲーミングモニター",
price: 45000,
category: "electronics",
tags: ["gaming", "monitor", "4k"],
inStock: true,
rating: 4.7,
releaseDate: new Date("2022-12-01")
},
{
id: 3,
name: "コーヒーメーカー",
price: 15000,
category: "home",
tags: ["kitchen", "coffee", "automatic"],
inStock: true,
rating: 3.8,
releaseDate: new Date("2024-01-10")
},
{
id: 4,
name: "デザイナーチェア",
price: 32000,
category: "furniture",
tags: ["chair", "ergonomic", "office"],
inStock: true,
rating: 4.2,
releaseDate: new Date("2023-08-22")
}
];
柔軟なフィルター条件をカリー化で実装
// 価格範囲でフィルタリング
const priceRange = (min: number, max: number) =>
(product: Product): boolean => product.price >= min && product.price <= max;
// カテゴリでフィルタリング
const inCategory = (category: string) =>
(product: Product): boolean => product.category === category;
// タグを含むかでフィルタリング
const hasTag = (tag: string) =>
(product: Product): boolean => product.tags.includes(tag);
// 在庫があるかでフィルタリング
const isInStock = () =>
(product: Product): boolean => product.inStock;
// 評価が一定以上でフィルタリング
const minRating = (rating: number) =>
(product: Product): boolean => product.rating >= rating;
// 特定の日付以降に発売されたものでフィルタリング
const releasedAfter = (date: Date) =>
(product: Product): boolean => product.releaseDate >= date;
汎用的なOR条件とAND条件の実装
先ほどのユーザー例でAND条件のpipePredicate
関数を作りましたが、ここではより汎用的に型引数を使って実装しましょう。そしてOR条件も同様に実装します。
// 汎用的なAND条件(すべての条件を満たす)
const pipePredicateGeneric = <T>(predicates: Array<(item: T) => boolean>) =>
(item: T): boolean => predicates.every((fn) => fn(item));
// 汎用的なOR条件(いずれかの条件を満たす)
const orPredicate = <T>(predicates: Array<(item: T) => boolean>) =>
(item: T): boolean => predicates.some((fn) => fn(item));
複雑な検索条件の実装例
以下の条件で商品を検索してみましょう:
- 電子機器カテゴリで、かつ
- 12万円以下で、かつ
- 在庫があり、かつ
- 「gaming」か「business」のタグがついているもの
// AND条件とOR条件を組み合わせる
const searchResults = products.filter(
pipePredicateGeneric<Product>([
inCategory("electronics"),
priceRange(0, 120000),
isInStock(),
orPredicate<Product>([
hasTag("gaming"),
hasTag("business")
])
])
);
console.log(searchResults);
検索条件を動的に構築する
実際のアプリケーションでは、ユーザーの選択に基づいて検索条件を動的に構築することがあります。そのような場合も、カリー化された関数を使うことで柔軟に対応できます。
// ユーザー入力から検索条件を構築する関数
function buildFilters(options: {
category?: string;
minPrice?: number;
maxPrice?: number;
inStockOnly?: boolean;
tags?: string[];
minRating?: number;
}): (product: Product) => boolean {
const filters: Array<(product: Product) => boolean> = [];
if (options.category) {
filters.push(inCategory(options.category));
}
if (options.minPrice !== undefined || options.maxPrice !== undefined) {
const min = options.minPrice ?? 0;
const max = options.maxPrice ?? Infinity;
filters.push(priceRange(min, max));
}
if (options.inStockOnly) {
filters.push(isInStock());
}
if (options.tags && options.tags.length > 0) {
const tagFilters = options.tags.map(tag => hasTag(tag));
filters.push(orPredicate<Product>(tagFilters));
}
if (options.minRating !== undefined) {
filters.push(minRating(options.minRating));
}
return filters.length > 0 ? pipePredicateGeneric<Product>(filters) : () => true;
}
// 使用例
const userFilters = buildFilters({
category: "electronics",
minPrice: 45000,
inStockOnly: true,
tags: ["monitor"]
});
const userSearchResults = products.filter(userFilters);
console.log(userSearchResults);
まとめ
カリー化と関数合成は、TypeScriptのような強力な型システムと組み合わせることで、より柔軟で再利用可能、そして型安全なコードを書くための強力なツールとなります。
技術 | ユースケース | メリット |
---|---|---|
カリー化 | 引数の部分適用 | 条件関数の動的生成、再利用性の向上 |
関数合成 | データ変換パイプライン | 処理の流れを明確に、可読性の向上 |
AND/OR条件 | 複雑な検索条件 | 柔軟なフィルタリングシステムの構築 |
これらの技術を組み合わせることで、宣言的でメンテナンスしやすいコードを書くことができます。特に大規模なアプリケーションや、頻繁に変更される要件に対して、柔軟に対応できるようになるでしょう。