カリー化

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

カリー化

このレッスンで分かること

  • カリー化 は、複数の引数を取る関数を、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 というプログラミング言語の名前も同じ人物に由来します。

本レッスンでは「整数 ab を受け取り、add という 2 引数関数をカリー化してから curriedAdd(a)(b) の形で呼び出し、その結果を返す」関数を作ります。最終的な戻り値は単に a + b ですが、カリー化された関数を内部で組み立てて適用する ところが学びどころです。

カリー化は 関数を分割して、引数を 1 つずつ受け取る 仕組み。部分適用との相性が良い。

何が嬉しいのか

直感的には「add(a, b) で済むのに、なぜ add(a)(b) のような面倒な形にするのか」と思います。カリー化の利点は 部分適用 です。

Python

add5 = curriedAdd(5) add5(3) # -> 8 add5(10) # -> 15

curriedAdd(5) で「5 を足す関数」を作っておき、後で別の値に適用できます。これにより 関数を作り回せる ようになり、汎用処理を簡潔に書けます。

動作のイメージ

diagram (will load when visible)

図のポイント (テキスト併記)

  • 図のように、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 + yy は後で受け取る引数、xクロージャ によって覚えられた最初の引数です。

JavaScript での実装

JavaScript

function curriedAddAndApply(a, b) { const curriedAdd = (x) => (y) => x + y; const addX = curriedAdd(a); return addX(b); }

JS のアロー関数を使うとさらに短く書けます。(x) => (y) => x + y2 段階のアロー関数 で、これがカリー化の最小形です。

クロージャを理解する

カリー化が成立するのは クロージャ という仕組みのおかげです。クロージャは「関数が定義された環境の変数を覚えている」性質を指します。curriedAdd(5) が返す関数は、5 を覚えたまま 後で呼び出されます。これは関数型の中核となる概念で、mapfilter のコールバックも同じ仕組みで動いています。

Python

def makeMultiplier(n): return lambda x: x * n times3 = makeMultiplier(3) times3(5) # -> 15

times3n = 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 つができることを確認しましょう。

  1. カリー化 の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. カリー化 とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. 関数の中で curriedAdd のような関数を定義し、curriedAdd(a)(b) の形で呼び出す
  2. curriedAdd(x) は (y) => x + y のような関数を返すこと
  3. 戻り値は整数。式は 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

ヒント

main.py
main.py
学習モード

メモ

カリー化

⌘S で保存