閉じる

カリー化でコードがすっきり!よくある書き方との違いとメリットを解説

「カリー化」という言葉を聞いたことはあっても、実際のコードでどう役立つのか、なかなかイメージしづらいかもしれません。

この記事では、JavaScriptとTypeScriptを中心に、カリー化の基本から具体的な書き方、メリット・デメリット、そして現場で使える応用例までを丁寧に解説します。

よくある書き方との違いや、どのような場面でコードがすっきりするのかを、実際のコード例を交えながら見ていきましょう。

カリー化とは何か

カリー化(currying)の基本的な意味

カリー化(currying)とは、「複数の引数を取る関数」を「引数1つを取る関数の連鎖」に変換するテクニックのことです。

数学者 Haskell Curry にちなんで名付けられました。

通常の関数は次のように複数の引数をまとめて受け取ります。

JavaScript
function add(a, b) {
  return a + b;
}
const result = add(2, 3); // 5

これをカリー化すると、次のようになります。

JavaScript
const add = a => b => a + b;
const result = add(2)(3); // 5

このように、カリー化された関数は、引数を1つ受け取るたびに次の関数を返し、最終的に結果を返すという動きをします。

普通の関数定義との違い

普通の関数とカリー化された関数の違いは、「いつ」「どのタイミングで」引数を渡すかにあります。

普通の関数では、必要な引数をすべてまとめて渡す必要があります。

JavaScript
const multiply = (a, b, c) => a * b * c;

multiply(2, 3, 4); // 24

一方、カリー化された関数では、引数を1つずつ、または途中までだけ渡すことができます。

JavaScript
const multiply = a => b => c => a * b * c;

multiply(2)(3)(4); // 24

// 途中まで渡しておいて、あとから残りを渡すこともできる
const times2 = multiply(2);   // b => c => 2 * b * c
const times2And3 = times2(3); // c => 2 * 3 * c

times2And3(4); // 24

同じ処理内容でも、「引数の受け取り方」と「関数の再利用のしやすさ」が変わるのが、カリー化の特徴です。

部分適用(partial application)との関係

カリー化とよくセットで語られる概念が、部分適用(partial application)です。

両者は似ていますが、厳密には別物です。

  • カリー化: 「n個の引数を取る関数」を「1個の引数を取る関数をn回ネストした形」に変換すること
  • 部分適用: 「一部の引数だけ先に固定した新しい関数」を作ること

例えば、非カリー化の関数を部分適用する例は次のようになります。

JavaScript
const add = (a, b) => a + b;

// a を 10 に固定した関数を作る(部分適用)
const add10 = b => add(10, b);

add10(5); // 15

同じことを、カリー化された関数で書くと、より自然に表現できます。

JavaScript
const add = a => b => a + b;

// a を 10 に固定した関数(これも部分適用)
const add10 = add(10);

add10(5); // 15

カリー化は「部分適用をしやすくするための関数の形」だと考えると理解しやすくなります。

カリー化の具体的な書き方

非カリー化の関数の例と問題点

まずは、よくある「引数をまとめて渡す」書き方から見ていきます。

例えば、ログ出力を行う関数を考えます。

JavaScript
function log(level, tag, message) {
  console.log(`[${level}] [${tag}] ${message}`);
}

log("INFO", "User", "ログインしました");
log("WARN", "User", "パスワード試行回数が多すぎます");

この書き方自体は問題ありませんが、現場で使うと次のような課題が出てきます。

  • 同じ level や tag を何度も書くことになり、重複が増える
  • 特定の level や tag を固定した関数を簡単に作りにくい
  • コールバック内などで使うときに、引数の順序を意識して wrap する必要がある

たとえば、“INFO レベルで User タグのログを頻繁に出したい”場合、次のように毎回3つの引数を書くことになります。

JavaScript
log("INFO", "User", "ログインしました");
log("INFO", "User", "プロフィールを更新しました");
log("INFO", "User", "ログアウトしました");

このような場面で、カリー化が威力を発揮します。

カリー化した関数の書き方(引数1つずつ)

上記のログ関数をカリー化してみます。

JavaScript
const log = level => tag => message => {
  console.log(`[${level}] [${tag}] ${message}`);
};

この形にすると、次のようなステップで関数を「分解」しながら使えます。

JavaScript
const infoLog = log("INFO");      // tag => message => ...
const infoUserLog = infoLog("User"); // message => ...

infoUserLog("ログインしました");
infoUserLog("プロフィールを更新しました");
infoUserLog("ログアウトしました");

「情報の一部を先に固定しておき、残りをあとから渡す」という使い方が自然にできるようになり、コードがすっきりします。

このように、カリー化は「意味のある単位ごとに引数をグループ化し、段階的に固定していく」ためのテクニックとも言えます。

JavaScriptでのカリー化の書き方

JavaScriptでカリー化を書くときの定番パターンは、const f = a => b => c => ...;のようなアロー関数のネストです。

手書きでカリー化する

まずはシンプルに手書きでカリー化してみます。

JavaScript
// 2つの数を足す関数(カリー化)
const add = a => b => a + b;

add(1)(2); // 3

3引数以上でも同様です。

JavaScript
const calcPrice = taxRate => discount => price => {
  const taxed = price * (1 + taxRate);
  return Math.round(taxed - discount);
};

const calcWith10PercentTax = calcPrice(0.1);
const calcWith10PercentTaxAnd500Off = calcWith10PercentTax(500);

calcWith10PercentTaxAnd500Off(2000); // 税込み2200 - 500 = 1700

ユーティリティ関数でカリー化する

毎回手で a => b => c => と書くのが面倒な場合は、カリー化用のヘルパー関数を自前で用意したり、ライブラリ(lodash/fp, Ramda など)を使うことができます。

簡易的な curry を自作すると、次のようになります。

JavaScript
// 簡易バージョン(引数の数を f.length で取得)
const curry = f => {
  return function curried(...args) {
    if (args.length >= f.length) {
      return f(...args);
    } else {
      return (...nextArgs) => curried(...args, ...nextArgs);
    }
  };
};

const add = (a, b) => a + b;
const curriedAdd = curry(add);

curriedAdd(1)(2);   // 3
curriedAdd(1, 2);   // 3 (まとめて渡してもOK)
curriedAdd(1)(2, 3); // ここでは 1,2,3 が渡るので 3引数の add ではエラーになる仕様など、実装に応じて振る舞いが決まる

カリー化をヘルパーで自動化するか、手書きするかは、プロジェクトのコーディングスタイルやチームの合意に応じて選ぶとよいでしょう。

TypeScriptでのカリー化と型定義

TypeScriptでは、カリー化した関数の型をしっかり定義しておくことが重要です。

型安全を維持しながらカリー化を行うことで、エディタの補完や型チェックの恩恵を最大限に受けられます。

手書きのカリー化と型

先ほどの calcPrice を TypeScript で書くと次のようになります。

JavaScript
const calcPrice =
  (taxRate: number) =>
  (discount: number) =>
  (price: number): number => {
    const taxed = price * (1 + taxRate);
    return Math.round(taxed - discount);
  };

const calcWith10PercentTax = calcPrice(0.1); // (discount: number) => (price: number) => number

このように、引数1つごとに矢印関数を区切り、その都度型を書くことで、型推論が段階的に効くようになります。

汎用的な curry 型を定義する

より汎用的な curry 関数を TypeScript で書くのはやや難易度が高めですが、2〜3引数程度であれば次のように書けます。

JavaScript
type Curry2<A, B, R> = (a: A) => (b: B) => R;

function curry2<A, B, R>(f: (a: A, b: B) => R): Curry2<A, B, R> {
  return (a: A) => (b: B) => f(a, b);
}

const add = (a: number, b: number) => a + b;
const curriedAdd = curry2(add);

const add10 = curriedAdd(10); // (b: number) => number
const result = add10(5);      // 15

3引数版も同様に定義できます。

JavaScript
type Curry3<A, B, C, R> = (a: A) => (b: B) => (c: C) => R;

ジェネリクスと条件付き型を駆使すると「任意個数の引数に対応する curry」を書くこともできますが、複雑になるため、実務では 既存のFPライブラリやユーティリティを利用することが多いです。

関数を返す関数(高階関数)との違い

カリー化された関数は「関数を返す関数」なので、高階関数(higher-order function)の一種です。

ただし、「カリー化である」ことと「高階関数である」ことはイコールではありません

  • 高階関数: 関数を受け取る、または関数を返す関数の総称
  • カリー化: 「複数引数の関数」を「引数1個ずつの関数の連鎖」に変換したもの

例えば、次の関数は高階関数ですが、特にカリー化された関数というわけではありません。

JavaScript
// 配列の各要素に f を適用する高階関数
const map = (array, f) => array.map(f);

// 関数を返すが、カリー化ではない例
const createRange = (start, end) => () => {
  const result = [];
  for (let i = start; i <= end; i++) {
    result.push(i);
  }
  return result;
};

一方、次は「高階関数」かつ「カリー化された関数」です。

JavaScript
const map = f => array => array.map(f);

カリー化は「高階関数を便利に扱うための1テクニック」であり、関数型プログラミングの文脈でよく使われるパターンだと理解するとよいでしょう。

カリー化のメリットとデメリット

コードがすっきり読みやすくなる理由

カリー化の大きなメリットの1つは、「文脈に応じて引数を自然な順序で埋めていける」ことです。

例えば、APIクライアントを考えてみます。

JavaScript
// 非カリー化
const request = (baseUrl, token, path) => {
  return fetch(`${baseUrl}${path}`, {
    headers: { Authorization: `Bearer ${token}` },
  });
};

// カリー化
const request = baseUrl => token => path =>
  fetch(`${baseUrl}${path}`, {
    headers: { Authorization: `Bearer ${token}` },
  });

カリー化されたバージョンだと、アプリの構成に沿った形で次のように段階的に固定できます。

JavaScript
const requestToApi = request("https://api.example.com");
const requestWithUserToken = requestToApi(userToken);

requestWithUserToken("/me");
requestWithUserToken("/orders");

アプリの「レイヤー」や「責任範囲」に応じて引数を分けて固定できるため、コードの意図が明確になり、読みやすさが向上します。

再利用しやすい小さな関数が作れる

カリー化された関数は、「途中まで引数を埋めた状態」の関数を簡単に作れるので、再利用性が高まります。

JavaScript
const contains = (search: string) => (target: string) =>
  target.includes(search);

const containsError = contains("ERROR");
const containsWarn = contains("WARN");

containsError("INFO: ok");     // false
containsError("ERROR: fail");  // true

このように、汎用的な処理をカリー化しておき、プロジェクト固有の意味を持つ関数として「部分適用」することで、読みやすくテストしやすいコードになります。

コールバックやイベント処理でのカリー化の利点

イベント処理やコールバックでは、「フレームワーク側が決めた引数の形」に自分の関数を合わせる必要があり、無名関数でラップしがちです。

例えば、React のイベントハンドラで次のようなコードを書いたことがあるかもしれません。

JavaScript
<button onClick={() => handleClick(userId, itemId)}>
  追加
</button>

これをカリー化を使って書き換えると、次のようにできます。

JavaScript
const handleClick = userId => itemId => () => {
  // 実際の処理
};

<button onClick={handleClick(userId)(itemId)}>
  追加
</button>

事前に文脈情報(userId, itemId など)を埋めておき、最後の段階でフレームワークのコールバックシグネチャに合わせることで、見通しの良いハンドラ登録ができるようになります。

テストコードやモック作成でのカリー化活用

テストコードやモックでは、同じ前提条件で異なる入力を試すことがよくあります。

このとき、カリー化した関数は非常に扱いやすくなります。

JavaScript
// バリデーションルールを表す関数
const minLength =
  (min: number) =>
  (value: string): boolean =>
    value.length >= min;

テストでは次のように書けます。

JavaScript
const min3 = minLength(3);

expect(min3("ab")).toBe(false);
expect(min3("abc")).toBe(true);
expect(min3("abcd")).toBe(true);

「min=3」という前提をカリー化で固定しておき、テストケースごとに value だけ変えるので、テストコードが単純になり、意図もわかりやすくなります。

同様に、モック関数を作るときにも「環境」や「設定値」を先に固めた関数を量産しやすくなり、テスト用のヘルパーが整理されます。

カリー化の注意点(可読性・パフォーマンス)

カリー化にはメリットだけでなく、注意するべきポイントもあります。

1つは可読性です。

カリー化を乱用すると、かえって「関数が関数を返しすぎてよくわからない」状態になります。

JavaScript
// 悪い例: 意味のまとまりがないまま、なんでもかんでもカリー化
const f = a => b => c => d => e => /* ... */ ;

このような場合、どの引数がどの段階で使われるのかがコードから読み取りづらくなります。

「意味のある単位ごと」に段階を分ける「必要な箇所だけ」カリー化するといったバランスが大切です。

もう1つはパフォーマンスです。

JavaScript において、関数をたくさん作ること自体にはコストがかかります。

  • ネストが深すぎるカリー化
  • 大量の要素に対してカリー化した関数を都度生成する map / filter

などでは、パフォーマンスへの影響が無視できなくなる場合があります。

とはいえ、通常のWebアプリケーション規模であれば多くの場合ボトルネックにはなりません。

「読みやすさ・保守性のためにカリー化を使い、明らかに重い部分では避ける」という判断が現実的です。

カリー化の実用的な応用例

配列処理(map/filter)でのカリー化活用

配列処理は、カリー化の恩恵を受けやすい代表的な場面です。

例えば、次のようなコードを考えてみます。

JavaScript
const users = [
  { id: 1, role: "admin" },
  { id: 2, role: "user" },
  { id: 3, role: "admin" },
];

const isAdmin = user => user.role === "admin";

const adminUsers = users.filter(isAdmin);

さらに、「特定のロールを持つユーザを取り出したい」という関数をカリー化で表現すると、次のようになります。

JavaScript
const hasRole = role => user => user.role === role;

const isAdmin = hasRole("admin");
const isUser = hasRole("user");

const adminUsers = users.filter(isAdmin);
const normalUsers = users.filter(isUser);

「条件のテンプレート」をカリー化で作っておき、配列処理に差し込むと、処理の意図が明確になります。

設定値を固定した関数を量産する

カリー化は、「設定値を固定したバリエーション関数」を量産したいときにも便利です。

例として、APIクライアントの作成を考えます。

JavaScript
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";

const request =
  (baseUrl: string) =>
  (method: HttpMethod) =>
  (path: string) =>
  (body?: unknown) => {
    return fetch(`${baseUrl}${path}`, {
      method,
      body: body ? JSON.stringify(body) : undefined,
      headers: { "Content-Type": "application/json" },
    });
  };

この関数から、次のようなバリエーションを作れます。

JavaScript
const api = request("https://api.example.com");

const get = api("GET");
const post = api("POST");

const getUser = (id: number) => get(`/users/${id}`)();
const createUser = (user: unknown) => post("/users")(user);

「ベースURL」「HTTPメソッド」「パス」「ボディ」と、責務ごとに引数をカリー化しておくことで、あとから必要な形の関数を自由に組み立てやすくなります。

バリデーション関数でのカリー化

バリデーションは、まさにカリー化がフィットする領域です。

バリデーションルールを「設定値」と「チェック対象」の2段階に分けたい場面が多いからです。

JavaScript
const minLength =
  (min: number) =>
  (value: string): boolean =>
    value.length >= min;

const maxLength =
  (max: number) =>
  (value: string): boolean =>
    value.length <= max;

const matches =
  (pattern: RegExp) =>
  (value: string): boolean =>
    pattern.test(value);

これらを組み合わせて、フォームごとにルールを定義できます。

JavaScript
const usernameValidators = [
  minLength(3),
  maxLength(20),
  matches(/^[a-z0-9_]+$/),
];

const isValidUsername = (value: string) =>
  usernameValidators.every(validate => validate(value));

数値や正規表現などの「設定値」でルールをカスタマイズし、それを value に適用するという構造が、カリー化で非常に書きやすくなります。

ログ出力やデバッグ関数をカリー化する

先ほど触れたログ関数の例を、もう少し現実的な形に広げてみます。

JavaScript
type LogLevel = "debug" | "info" | "warn" | "error";

const logger =
  (level: LogLevel) =>
  (tag: string) =>
  (...args: unknown[]) => {
    const prefix = `[${level.toUpperCase()}] [${tag}]`;
    console.log(prefix, ...args);
  };

この関数から、様々なロガーを派生させられます。

JavaScript
const appInfo = logger("info")("App");
const userWarn = logger("warn")("User");
const httpError = logger("error")("HTTP");

appInfo("アプリを起動しました");
userWarn("不正な入力です", { value: "..." });
httpError("リクエスト失敗", { status: 500 });

「ログレベル」「タグ」を先に固定した関数を作り、その後で実際のログ内容を渡すことで、ログ出力の一貫性が保ちやすくなり、呼び出し側のコードもシンプルになります。

また、テスト環境では、loggerの実装を差し替えても、同じカリー化されたインターフェースのまま利用できるため、切り替えもしやすくなります。

小さなステップでカリー化を学ぶ練習方法

カリー化は概念としてはシンプルですが、「どこで・どう使うか」を体に覚えさせるには、少し練習が必要です。

おすすめのステップをいくつか紹介します。

  1. 2引数関数のカリー化を繰り返す
    add(a, b)concat(a, b) など、シンプルな2引数関数を a => b => ... 形式に書き換えてみます。
  2. 部分適用用のヘルパー関数を自分で書いてみる
    例えば、partial(f, a) のように、先頭の引数だけ固定する関数を作り、カリー化との違いを体感します。
  3. 既存コードの「同じ引数を何度も書いている場所」を探す
    例えばログやAPIクライアント、バリデーションなどで、毎回同じ設定値を渡しているコードを見つけたら、そこをカリー化でリファクタリングしてみます。
  4. 配列処理のユーティリティ関数をカリー化してみる
    map(array, f)map(f)(array) のように、引数の順序も含めて「よく使う順」に並べ替えながらカリー化してみると、実用上の感覚がつかみやすくなります。

小さな練習から始めて、徐々に自分のプロジェクトのコードへ適用範囲を広げていくのが、カリー化を身につける近道です。

まとめ

カリー化は、「複数の引数を取る関数」を「引数1つずつを受け取る関数の連鎖」に変えるテクニックです。

これにより、部分適用がしやすくなり、設定値や文脈を段階的に固定した関数を簡単に作れるようになります。

JavaScript / TypeScript では、アロー関数をネストして書くことで手軽にカリー化を実現できますし、型定義を工夫すれば、型安全に活用することも可能です。

配列処理、バリデーション、APIクライアント、ログ出力、イベントハンドラなど、実際の現場でよく出会うパターンと相性が良いのも特徴です。

一方で、やみくもにカリー化するとコードが読みにくくなったり、不要な関数生成でパフォーマンスに影響が出たりすることもあるため、「意味のある単位で引数を分ける」「必要な場所に絞って使う」というバランス感覚が大切です。

まずは、自分のコードの中で「同じ引数を何度も書いている場所」や「設定値を固定したバリエーション関数がほしい場所」を見つけ、そこからカリー化を試してみるとよいでしょう。

少しずつ使いどころが見えてくると、カリー化はコードをすっきりさせる強力な道具として、自然にあなたの手になじんでいきます。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!