0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-04-13

はじめに

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条件 複雑な検索条件 柔軟なフィルタリングシステムの構築

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?