はじめに
カリー化関数をご存知でしょうか。 xxx(a)(b) のように呼び出す関数ですね。
ReactやTypeScriptを使用したフロントエンド開発で見かけることも多く、「なんとなくは知ってる」という方も多いのではないでしょうか。
ただ、カリー化関数の見た目や書き方だけを捉えようとすると、以下のような疑問も出てきます。
- 引数をまとめて渡すのではあれば、
xxx(a, b)のようなシンプルな普通の関数でも良いのでは? - あえて
xxx(a)(b)で書く意図は?
こうした視点に対して、自分なりの考えを整理してみる機会があったので、備忘録も兼ねてまとめることにしました。
本記事では、カリー化関数の仕組みそのものや関数型プログラミングの理論などには深く踏み込まず、「カリー化関数を使うべき場面」について、実務的な観点から考えていきたいと思います。
そもそもカリー化関数とは?
まずは、カリー化関数がどのようなものかを簡単に確認しておきましょう。
ざっくり言えば、「呼び出した関数の戻り値が更に別の関数になっていて、それに対して更に引数を渡す」という構造を持つ関数が、いわゆる「カリー化(currying)」された関数です。
先にも述べたように、カリー化関数は xxx(a)(b) の形で呼び出されます。以下はその例です。
// カリー化関数
const xxx = (a: string) => {
// a を使った何らかの処理
return (b: number) => {
// b を使った何らかの処理
return `${a}-${b}`; // aもクロージャで参照可能
};
};
// カリー化関数(xxx(a)(b))を呼び出す
const result = xxx("yakisoba")(123);
// -> "yakisoba-123"
このような関数はReactコンポーネントでも使用でき、イベントハンドラを動的に生成したい場合などに利用されます。
以下はそのコード例です。
const handleClickFoodStall = (foodStall: string) => (e: React.MouseEvent<HTMLButtonElement>) => {
console.log(`Summer festival food stalls: ${foodStall} clicked`, e);
};
<button onClick={handleClickFoodStall("yakisoba")}>焼きそば</button>
<button onClick={handleClickFoodStall("shavedIce")}>かき氷</button>
「カリー化関数」という言葉に聞き馴染みがなかったとしても、ReactやTypeScriptを使っている方であれば、”目にしたことがあった”という方も多いのではないでしょうか。
ただ、改めて仕組みを確認してみた時、特にひとつ目の例に対して「それ、xxx(a, b) ではダメなの?」なんて疑問に思う方もいるかもしれません。
xxx(a, b) の方が適しているケース
実際、実務の中では普通の関数構造… つまりxxx(a, b) のように引数をまとめて渡す形で十分なケースの方が多いです。
特に、以下のような場合では、あえて関数を分けずにまとめて書いた方が自然で、扱いやすいかと思います。
-
関数の呼び出しが一度きりのケース
- 再利用性や抽象度を高める必要がなければ、わざわざ関数を分ける意義はあまりありません
-
引数が毎回ペアで渡されるケース
- a と b の関係が密接で、どちらか一方だけを先に決めておく必要性がない場合は、単にまとめて渡す方がわかりやすくなります
-
関数構造をシンプルなまま保ちたいケース
- 特にユーティリティ系の関数や、ある程度スコープの狭い処理では、見た目や可読性の観点からもシンプルな構造の方が望ましいことが多いです
カリー化関数は便利なテクニックですが、「使えるから使う」ではなく、「本当に必要な場面か」を考えることが大切です。
まずは xxx(a, b) で書いてみて、再利用性や引数の渡し方に課題が出てきたらカリー化を検討する、くらいのスタンスが良いのかもしれません。
カリー化関数の方が適しているケース
「部分適用したい」から逆算して選ぶ構造
実務でカリー化関数が使われる場面の多くは、「この関数を一部の引数を先に固定して使い回したい」という「部分適用のニーズ」から始まります。
たとえば:
- バリデーション関数でルールだけ先に設定しておきたい
- コンテキスト付きのロガーを量産したい
- ユーザーIDごとのイベントハンドラを作りたい
こうした「一部の引数だけ先に決めたい」という意図が先にあり、その実現手段として xxx(a)(b) のようなカリー化関数を使用した構成にする、というのが実務的な流れと言えるでしょう。
補足:カリー化と部分適用の関係
詳しい説明に入る前に、簡単にカリー化と部分適用の関係について確認しておきましょう。
| 概念 | 役割 | 関係性 |
|---|---|---|
| カリー化 | 関数を1引数ずつ受け取る構造に変換する | 部分適用の前提になる |
| 部分適用 | 引数を一部だけを先に与えて、新しい関数を返す | カリー化関数の活用方法 |
つまり、カリー化は「構造の変換」、部分適用は「その構造の活用して再利用しやすくする手段」という位置付けになります。
この対比で捉えると、思想的な整理がしやすいです。
なお、カリー化していない関数でも部分適用は可能です。
ただし、自作のユーティリティ関数やRamdaなどの関数ライブラリを使用するといった工夫が必要であること、また可読性や直感的な使いやすさの観点から、カリー化した構造を使用して部分適用を行う方が有用な場面が多いかと思います。
可読性や構造の整理への貢献
xxx(a)(b) のような形は、単なる部分適用のためだけでなく、関数の「設計意図」を読み手に伝える上でも有用な場面があります。
1. 役割の分離が明確に見える
-
xxx(a)の時点で「設定や前処理」を与えている(= 共通処理を用意している) -
(...)(b)の部分で「データやイベントを処理」している(= その共通処理にデータを流し込んでいる)
この構造によって、関数が「共通ロジック」と「動的な入力」に分かれていることが、コードから視覚的に読み取ることができるため、読み手が構造を頭の中で展開しやすくなります。
2. 再利用意図が明確に伝わる
const validateWithRules = validate(rules);
validateWithRules("abc");
validateWithRules("123");
一部の引数をバインドした関数に、それがわかるような名前をつけて使用することで、「再利用のために分けている」ことを自然に伝えることができます。
3. 関数合成との親和性が高い
import { pipe } from 'fp-ts/function'
const transform = pipe(
sanitize(config),
validate(rules),
saveToDB
);
前処理や設定値を先にバインドした関数はパイプライン構造に組み込みやすく、処理の意図がストレートに読めるという利点もあります。
使用例から考える実務の視点
先にも述べたように、カリー化を行うかどうかの出発点は「部分適用したい」という設計ニーズにあります。
具体的に言うと、
再利用したい/冗長さを減らしたい
↓
先に一部の引数を固定しよう
↓
それにはカリー化関数が適している
というような流れです。
「理論」から入るのではなく、「”設計上のニーズ”から逆算して構造を選ぶ」という視点を持てると、実務でも活用できる一段上の関数設計となるのではないでしょうか。
では実際に、実務でよくあるカリー化関数の使用例をいくつか取り上げ、どのような視点からカリー化関数を選択したのか、考えてみましょう。
1. ロガーの文脈固定
const withLogger = (context: string) => (message: string) => {
console.log(`[${context}] ${message}`);
};
const userLogger = withLogger("User");
userLogger("Logged in");
userLogger("Updated profile");
const systemLogger = withLogger("System");
systemLogger("Restarting...");
実務の視点:
毎回 context を渡すのは冗長
→ context を先に与える(=文脈を固定しておく)ことで、文脈付きの関数を用意
→ 再利用性が向上
2. Reactイベントハンドラの量産
const handleClickFactory = (userId: string) => (e: React.MouseEvent<HTMLButtonElement>) => {
console.log(`${userId} clicked`, e);
};
// ボタン複数に個別ユーザーIDの処理を割り当てる
<button onClick={handleClickFactory("user1")}>User1</button>
<button onClick={handleClickFactory("user2")}>User2</button>
実務の視点:
userId を個別に持ったイベントハンドラ(onClick)を生産したい
→ 関数をカリー化して、一部をあらかじめバインドしておく
→ onClick の中で直接 userId を渡す形で量産できる
3. ルールベースのバリデーション
type Rule = (input: string) => boolean;
const validate = (rules: Rule[]) => (input: string) => {
return rules.every(rule => rule(input));
};
3-1. 特定のルールを複数箇所で使用
// ルール定義
const minLength = (min: number): Rule => input => input.length >= min;
const mustContainNumber: Rule = input => /\d/.test(input);
const userInputRules = [
minLength(6),
mustContainNumber
];
// バリデータを構築(部分適用)
const validateUserInput = validate(userInputRules);
console.log(validateUserInput("abcABC"));
// -> false
console.log(validateUserInput("abc123"));
// -> true
3-2. 動的にルールセットを切り替える
// ルール定義
const shortRules = [minLength(3)];
const strictRules = [minLength(10), mustContainNumber];
const shortValidator = validate(shortRules);
const strictValidator = validate(strictRules);
console.log(shortValidator("a1"));
// -> false
console.log(shortValidator("abc123"));
// -> true
console.log(strictValidator("abc123"));
// -> false
console.log(strictValidator("abcABC1234"));
// -> true
実務の視点:
rules を毎回渡すと冗長かつ重複
→ 部分適用して、一度設定しておいた関数を使い回す形にしたい
→ 可読性、再利用性の向上
このような文脈では「可読性 = 意図の明確さ」として機能します。
意図が明確に伝わる場面であれば、xxx(a)(b) のような形の方が可読性が高い、と言えるでしょう。
カリー化を行う際の注意点
適用しすぎると逆効果にも
初見の人にとっては xxx(a)(b)(c)... のように続くと、「何回関数を返すのか」「何を固定したくて、何が毎回変わるのか」など混乱の元になりかねません。
そのため、カリー化は「関数の設計意図が読み手に伝わる場合」に限って使用するのが良いでしょう。
カリー化するかの判断基準をあらかじめ設定しておくと、設計の際にスムーズです。
以下はその例です。関数設計時の参考になれば幸いです。
カリー化の設計判断:
- 何かの「設定値」や「文脈」など、先に渡したいものがあるか
- 同じ処理を複数の場面で使い回したいか
- 再利用が前提となる処理か(=一度きりの処理でない)
- 分けることで処理の意味がより明確になるか
多段カリー化は慎重に
理論上、カリー化は何段でも可能です。
数学的には「1つの引数しか取らない関数の合成」という純粋な関数型の考え方に基づいており、以下のように連続して関数を返すこともできます。
const summerFestivalFoodStalls = (area: "kitaku") => (type: "heating") => (userId: "user1") => (foodStalls: "yakisoba") => result;
上記の例からもおわかりいただけるかと思いますが、以下のような理由から実務では2段階程度(多くて3段階)が現実的な階層と言えます。
- 読み手が処理を追いにくくなる
- 型推論が複雑化する
- 特にTypeScriptでは
ReturnType<ReturnType<typeof f>>のようになりがち
- 特にTypeScriptでは
- デバッグしづらい
- 使い回しの意図が薄れる
おわりに
いかがでしたでしょうか。
今回はカリー化関数について、実務の目線からその使い所を考えてみました。
実際にカリー化を導入する際は、チームやプロジェクトで以下の点が共有できていると効果的な利用ができると思います。
「どのような意図で分割しているのか」
「多段カリー化をどこまで許容するか」
「部分適用の際の命名ルール(関数が何を返すのかがわかる命名にする、など)」
カリー化は目的ではなく手段である、という視点を持てると、関数設計時の有用な選択肢となるのではないでしょうか。
今回整理した内容を踏まえて、実際の関数処理でカリー化できそうか、した方が良いのか、考えながら実装できるように意識していきたいと思います。
以上です。最後まで閲覧いただきありがとうございます。
参考
関数型プログラミングについて
カリー化関数・部分適用について
関数ライブラリ
- https://ramdajs.com/docs/#curry
- https://gcanti.github.io/fp-ts/guides/upgrade-to-v2.html?utm_source=chatgpt.com#the-new-api
その他