TypeScriptで学ぶ、カリー化と関数合成を使った実用的なフィルター実装

## はじめに

JavaScript/TypeScriptで関数をより柔軟に再利用するためのテクニックとして「カリー化」と「関数合成」があります。
この記事では、これらの概念を実務的なコードとともに解説し、特にフィルター処理にどう活かせるかを紹介します。

## カリー化（Currying）とは？

カリー化とは、**複数の引数をとる関数を、1引数ずつの関数に分解すること**です。

```ts
// 通常の関数
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つの処理にすること**です。

```ts
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))` のように順番に適用される**のです。

### 使用例：
```ts
const sanitize = compose(toLower, trim);
console.log(sanitize("  HeLLo  ")); // "hello"
```

この場合、`"  HeLLo  "` がまず `trim` により余分なスペースを除去され、次に `toLower` により小文字化されます。


## カリー化を使った柔軟なフィルター処理の実装

### ユーザーデータとその型定義

まず、フィルタリングするデータの型を明示的に定義しましょう。

```ts
// ユーザー型の定義
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",
  },
];
```

### カリー化した関数を使って条件を作る

ユーザーをフィルターするための条件関数を、カリー化を使って実装します。

```ts
// ユーザータイプでフィルタリングする関数
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条件（すべての条件を満たす）で合成する関数を定義します。

```ts
// 複数の述語関数を受け取り、それらをAND条件で合成する関数
const pipePredicate = (predicates: Array<(user: User) => boolean>) => 
    (user: User): boolean => predicates.every((fn) => fn(user));
```

### 使用例：条件を合成してフィルターする

定義した条件関数を組み合わせてフィルタリングしてみましょう。

```ts
// 複数条件でフィルタリング
const filtered = users.filter(
    pipePredicate([
        hasType("subscriber"),    // タイプが "subscriber" のユーザー
        fromCountry(1),           // 国コードが 1 のユーザー
        bornBefore(1995),         // 1995年以前に生まれたユーザー
    ])
);

console.log(filtered);
```

この例では、次の条件に一致するユーザーだけが返されます：

- type が "subscriber"
- countryCode が 1
- dateOfBirth が1995年以前

このように、条件関数を柔軟に組み合わせて、読みやすく再利用可能なフィルター処理が可能になります。カリー化を利用することで、条件の一部だけを事前に適用した新しい関数を作ることができ、より柔軟なフィルター処理が実現できます。

## 実践的な例：複雑なフィルタリングシステムの構築

上記で紹介した技術を使って、より実践的なフィルタリングシステムを構築してみましょう。例えば、ECサイトで商品を検索するケースを考えてみます。

### 商品データの例

```ts
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")
    }
];
```

### 柔軟なフィルター条件をカリー化で実装

```ts
// 価格範囲でフィルタリング
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条件も同様に実装します。

```ts
// 汎用的な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」のタグがついているもの

```ts
// AND条件とOR条件を組み合わせる
const searchResults = products.filter(
    pipePredicateGeneric<Product>([
        inCategory("electronics"),
        priceRange(0, 120000),
        isInStock(),
        orPredicate<Product>([
          hasTag("gaming"),
          hasTag("business")
        ])
    ])
);

console.log(searchResults);
```

### 検索条件を動的に構築する

実際のアプリケーションでは、ユーザーの選択に基づいて検索条件を動的に構築することがあります。そのような場合も、カリー化された関数を使うことで柔軟に対応できます。

```ts
// ユーザー入力から検索条件を構築する関数
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条件 | 複雑な検索条件 | 柔軟なフィルタリングシステムの構築 |

これらの技術を組み合わせることで、宣言的でメンテナンスしやすいコードを書くことができます。特に大規模なアプリケーションや、頻繁に変更される要件に対して、柔軟に対応できるようになるでしょう。