コンテンツにスキップ

第5章: カリー化と部分適用

この章で学ぶこと

  • カリー化の概念と仕組み
  • 部分適用との違い
  • TypeScriptでのカリー化の実装
  • 実践的な使用例

カリー化とは?

カリー化(Currying)は、複数の引数を取る関数を、1つの引数を取る関数の連鎖に変換する技法です。

基本的な例

// 通常の関数
function add(a: number, b: number): number {
    return a + b;
}

// カリー化された関数
function addCurried(a: number): (b: number) => number {
    return (b: number) => a + b;
}

// 使用例
const add5 = addCurried(5);
console.log(add5(3)); // 8
console.log(add5(7)); // 12

// 直接呼び出し
console.log(addCurried(5)(3)); // 8

カリー化の実装

手動でのカリー化

// 3引数の関数をカリー化
function multiply(a: number): (b: number) => (c: number) => number {
    return (b: number) => (c: number) => a * b * c;
}

const result = multiply(2)(3)(4); // 24

// 段階的な適用
const multiplyBy2 = multiply(2);
const multiplyBy2And3 = multiplyBy2(3);
const final = multiplyBy2And3(4); // 24

汎用的なcurry関数

// 2引数関数用のcurry
function curry2<A, B, C>(
    fn: (a: A, b: B) => C
): (a: A) => (b: B) => C {
    return (a: A) => (b: B) => fn(a, b);
}

// 使用例
const add = (a: number, b: number) => a + b;
const curriedAdd = curry2(add);

console.log(curriedAdd(5)(3)); // 8

可変長引数のcurry

// より柔軟なcurry実装
function curry<T extends (...args: any[]) => any>(
    fn: T,
    ...args: any[]
): any {
    if (args.length >= fn.length) {
        return fn(...args);
    }
    return (...nextArgs: any[]) => 
        curry(fn, ...args, ...nextArgs);
}

// 使用例
const sum = (a: number, b: number, c: number) => a + b + c;
const curriedSum = curry(sum);

console.log(curriedSum(1)(2)(3)); // 6
console.log(curriedSum(1, 2)(3)); // 6
console.log(curriedSum(1)(2, 3)); // 6

部分適用との違い

部分適用(Partial Application)は、関数にいくつかの引数を固定して、残りの引数を受け取る新しい関数を作ることです。

// 部分適用
function partial<T extends any[], U extends any[], R>(
    fn: (...args: [...T, ...U]) => R,
    ...fixedArgs: T
): (...remainingArgs: U) => R {
    return (...remainingArgs: U) => 
        fn(...fixedArgs, ...remainingArgs);
}

// 使用例
const greet = (greeting: string, name: string, punctuation: string) => 
    `${greeting}, ${name}${punctuation}`;

const sayHello = partial(greet, "Hello");
console.log(sayHello("Alice", "!")); // "Hello, Alice!"

const sayHelloAlice = partial(greet, "Hello", "Alice");
console.log(sayHelloAlice(".")); // "Hello, Alice."

カリー化 vs 部分適用

カリー化 部分適用
常に1つの引数を取る関数を返す 任意の数の引数を固定できる
完全に適用されるまで関数を返し続ける 一度の適用で新しい関数を作る
数学的な概念に基づく より実用的なアプローチ

実践的な使用例

設定可能な処理の作成

// ログ出力関数
const log = curry(
    (level: string, timestamp: Date, message: string) => {
        console.log(`[${level}] ${timestamp.toISOString()}: ${message}`);
    }
);

// 特定のログレベルの関数を作成
const info = log("INFO");
const error = log("ERROR");
const debug = log("DEBUG");

// 現在時刻でログ出力
const now = new Date();
info(now)("Application started");
error(now)("Connection failed");

データ処理パイプライン

// フィルタリング関数
const filter = curry(
    <T>(predicate: (item: T) => boolean, array: T[]) => 
        array.filter(predicate)
);

// マッピング関数
const map = curry(
    <T, U>(fn: (item: T) => U, array: T[]) => 
        array.map(fn)
);

// 特定の条件でフィルタリング
const filterActive = filter((user: User) => user.isActive);
const filterAdults = filter((user: User) => user.age >= 18);

// 変換関数
const getName = map((user: User) => user.name);
const toUpperCase = map((name: string) => name.toUpperCase());

// パイプラインの構築
const getActiveAdultNames = pipe(
    filterActive,
    filterAdults,
    getName,
    toUpperCase
);

イベントハンドラーの作成

// イベントハンドラー生成関数
const createHandler = curry(
    (eventType: string, selector: string, callback: Function) => {
        document.querySelectorAll(selector).forEach(element => {
            element.addEventListener(eventType, callback as EventListener);
        });
    }
);

// 特定のイベントタイプのハンドラー
const onClick = createHandler("click");
const onHover = createHandler("mouseenter");

// 使用例
onClick(".button")(() => console.log("Button clicked"));
onClick(".link")(() => console.log("Link clicked"));
onHover(".card")(() => console.log("Card hovered"));

バリデーション関数の組み合わせ

// バリデーション関数
const minLength = curry(
    (min: number, value: string) => 
        value.length >= min || `Must be at least ${min} characters`
);

const maxLength = curry(
    (max: number, value: string) => 
        value.length <= max || `Must be at most ${max} characters`
);

const matches = curry(
    (pattern: RegExp, value: string) => 
        pattern.test(value) || `Does not match required pattern`
);

// 特定のバリデーションルール
const minLength8 = minLength(8);
const maxLength20 = maxLength(20);
const hasNumber = matches(/\d/);
const hasUpperCase = matches(/[A-Z]/);

// パスワードバリデーション
const validatePassword = (password: string) => {
    const validators = [minLength8, maxLength20, hasNumber, hasUpperCase];

    for (const validate of validators) {
        const result = validate(password);
        if (result !== true) {
            return { valid: false, error: result };
        }
    }

    return { valid: true };
};

実践演習

演習1: 計算機能のカリー化

以下の計算関数をカリー化してください:

function calculate(operation: string, a: number, b: number): number {
    switch (operation) {
        case "+": return a + b;
        case "-": return a - b;
        case "*": return a * b;
        case "/": return a / b;
        default: throw new Error("Unknown operation");
    }
}

// カリー化バージョンを実装
function calculateCurried(/* ... */) {
    // 実装
}
解答
function calculateCurried(operation: string) {
    return (a: number) => (b: number) => {
        switch (operation) {
            case "+": return a + b;
            case "-": return a - b;
            case "*": return a * b;
            case "/": return a / b;
            default: throw new Error("Unknown operation");
        }
    };
}

// 使用例
const add = calculateCurried("+");
const multiply = calculateCurried("*");

console.log(add(5)(3)); // 8
console.log(multiply(4)(7)); // 28

// 便利な関数を作成
const double = multiply(2);
const increment = add(1);

console.log(double(10)); // 20
console.log(increment(5)); // 6

演習2: 関数合成とカリー化

カリー化を使ってデータ変換パイプラインを作成してください:

type Product = {
    name: string;
    price: number;
    category: string;
};

// 以下の機能を実装
// 1. カテゴリーでフィルタリング
// 2. 価格に税金を追加
// 3. 価格でソート
解答
const filterByCategory = curry(
    (category: string, products: Product[]) =>
        products.filter(p => p.category === category)
);

const addTax = curry(
    (taxRate: number, products: Product[]) =>
        products.map(p => ({
            ...p,
            price: p.price * (1 + taxRate)
        }))
);

const sortByPrice = curry(
    (order: "asc" | "desc", products: Product[]) =>
        [...products].sort((a, b) => 
            order === "asc" ? a.price - b.price : b.price - a.price
        )
);

// パイプラインの作成
const getElectronicsWithTax = pipe(
    filterByCategory("Electronics"),
    addTax(0.1), // 10%の税金
    sortByPrice("asc")
);

// 使用例
const products: Product[] = [
    { name: "Laptop", price: 1000, category: "Electronics" },
    { name: "Shirt", price: 50, category: "Clothing" },
    { name: "Phone", price: 800, category: "Electronics" }
];

const result = getElectronicsWithTax(products);
// [{ name: "Phone", price: 880, ... }, { name: "Laptop", price: 1100, ... }]

まとめ

この章では、カリー化と部分適用について学びました:

  • ✅ カリー化は複数引数の関数を1引数関数の連鎖に変換
  • ✅ 部分適用は引数を段階的に固定する技法
  • ✅ カリー化により、設定可能で再利用可能な関数を作成できる
  • ✅ 関数合成と組み合わせることで、強力なデータ処理パイプラインを構築できる

次の章では、イミュータブルなデータ操作を実現する「関数型データ構造」について学びます。

第6章: 関数型データ構造 →