カリー化
カリー化
このレッスンで分かること
- カリー化 は、複数の引数を取る関数を、1 引数の関数の連なり に変換する技法です
- 直感的には「
add(a, b)で済むのに、なぜadd(a)(b)のような面倒な形にするのか」と思いますcurriedAdd(5)で「5を足す関数」を作っておき、後で別の値に適用できます
カリー化 とは
2 引数の関数を 1 引数の関数のチェーン
f(a)(b)に変換するカリー化を内部で組み立てて適用する。add(a, b)をカリー化してadd(a)(b)の形で呼び出した結果を返す関数を実装する。
カリー化 は、複数の引数を取る関数を、1 引数の関数の連なり に変換する技法です。たとえば add(a, b) を add(a)(b) の形にする操作です。論理学者ハスケル・カリー (Haskell Curry) にちなんで名付けられました。Haskell というプログラミング言語の名前も同じ人物に由来します。
本レッスンでは「整数 a と b を受け取り、add という 2 引数関数をカリー化してから curriedAdd(a)(b) の形で呼び出し、その結果を返す」関数を作ります。最終的な戻り値は単に a + b ですが、カリー化された関数を内部で組み立てて適用する ところが学びどころです。
カリー化は 関数を分割して、引数を 1 つずつ受け取る 仕組み。部分適用との相性が良い。
何が嬉しいのか
直感的には「add(a, b) で済むのに、なぜ add(a)(b) のような面倒な形にするのか」と思います。カリー化の利点は 部分適用 です。
Python
add5 = curriedAdd(5)
add5(3) # -> 8
add5(10) # -> 15curriedAdd(5) で「5 を足す関数」を作っておき、後で別の値に適用できます。これにより 関数を作り回せる ようになり、汎用処理を簡潔に書けます。
動作のイメージ
図のポイント (テキスト併記)
- 図のように、
curriedAdd(3)は 新しい関数 を返し、その関数に4を渡すと最終的な結果7が出ます
図のように、curriedAdd(3) は 新しい関数 を返し、その関数に 4 を渡すと最終的な結果 7 が出ます。
Python での実装
Python
def curriedAddAndApply(a, b):
def curriedAdd(x):
return lambda y: x + y
addX = curriedAdd(a)
return addX(b)curriedAdd(x) は 新しい関数 を返します。return lambda y: x + y の y は後で受け取る引数、x は クロージャ によって覚えられた最初の引数です。
JavaScript での実装
JavaScript
function curriedAddAndApply(a, b) {
const curriedAdd = (x) => (y) => x + y;
const addX = curriedAdd(a);
return addX(b);
}JS のアロー関数を使うとさらに短く書けます。(x) => (y) => x + y は 2 段階のアロー関数 で、これがカリー化の最小形です。
クロージャを理解する
カリー化が成立するのは クロージャ という仕組みのおかげです。クロージャは「関数が定義された環境の変数を覚えている」性質を指します。curriedAdd(5) が返す関数は、5 を覚えたまま 後で呼び出されます。これは関数型の中核となる概念で、map や filter のコールバックも同じ仕組みで動いています。
Python
def makeMultiplier(n):
return lambda x: x * n
times3 = makeMultiplier(3)
times3(5) # -> 15times3 は n = 3 を覚えているので、いつ呼んでも * 3 の動きをします。
クロージャは
関数が外側の変数を覚える性質。これがあるからカリー化や部分適用が成立する。
N 引数のカリー化
2 引数なら手書きで書けますが、N 引数 の関数をカリー化したい場合は再帰的な curry ヘルパーを書きます。Lodash の curry などはこれを汎用化したもので、curry((a, b, c) => a + b + c)(1)(2)(3) === 6 のように使えます。本レッスンでは 2 引数に限定して仕組みを掴むのが目的です。
よくある間違い
1 つ目は 呼び出し括弧を忘れる こと。curriedAdd(a)(b) のように 2 段階で呼ばないと結果が得られません。1 つしか呼ばないと 関数オブジェクトが返ってくる ので、テストが失敗します。2 つ目は クロージャの理解不足。curriedAdd(5) の 5 がどう保持されているかを意識しないと、デバッグで混乱します。3 つ目は 副作用を持ち込む こと。カリー化された関数は通常 純粋関数 であることが期待されます。グローバル変数を書き換えるとカリー化の利点が失われます。
やってみよう
curriedSub(a)(b) = a - bを作ってみる。引数の順序を間違えないこと。curriedAddを 3 引数curriedAdd(a)(b)(c)に拡張してみる。(x) => (y) => (z) => x + y + zの形。- カリー化された関数を
mapのコールバックとして渡してみる。nums.map(curriedAdd(10))のように使える。
カリー化 + クロージャは関数型の基礎中の基礎。
部分適用を使いこなせると、コードの再利用度が大きく上がる。
よくある質問
Q. 高階関数とは何ですか?
A. 関数を引数に取る、または関数を返す関数のことです。map / filter / reduce が代表で、コールバックを受け取って汎用化された処理を提供します。「処理の差分だけ渡せる」ため、ボイラープレートが減りコードが宣言的になります。
Q. カリー化はどんな場面で使いますか?
A. 引数を順番に部分適用したいときや、関数合成を綺麗に書きたいときに有効です。例えば add(a)(b) と書ければ add(5) で「5 を足す関数」が作れます。React のイベントハンドラ生成や、設定付きユーティリティ関数を作るのに便利です。
Q. 関数型と OOP はどっちを使うべき?
A. 状態を持つドメインモデルは OOP、データ変換パイプラインは関数型と使い分けるのが現代的です。Java も Stream で関数型を取り入れており、JS も class と高階関数を併用します。完全にどちらかに振らず、適材適所で組み合わせる設計が主流です。
次のレッスン
次は trie の単純検索 (prefix マッチ) で、trie の単純検索 (prefix マッチ) を学びます。
事前確認 — 進む前に次の 3 つができることを確認しましょう。
- カリー化 の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. カリー化 とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
- 関数の中で curriedAdd のような関数を定義し、curriedAdd(a)(b) の形で呼び出す
- curriedAdd(x) は (y) => x + y のような関数を返すこと
- 戻り値は整数。式は a + b と等価
入出力例
test-cases.txt
curriedAddAndApply(3, 4) → 7
curriedAddAndApply(0, 0) → 0
curriedAddAndApply(10, 5) → 15
curriedAddAndApply(-2, 5) → 3
curriedAddAndApply(100, 200) → 300
curriedAddAndApply(-5, -3) → -8