関数合成

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

関数合成

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

  • 関数合成は 抽象度の高い再利用 を可能にする
  • 関数合成 は数学にも出てくる基本概念で、2 つの関数 fg から f(g(x)) を作る操作を指します
  • addOne(x) = x + 1double(x) = x * 2 とします

関数合成 とは

compose(f, g)(x) = f(g(x)) の関数合成を関数の中で組み立てて適用する。addOnedouble を合成して値に適用した結果を返す関数を実装する。

関数合成 は数学にも出てくる基本概念で、2 つの関数 fg から f(g(x)) を作る操作を指します。記号で書くと (f ∘ g)(x) = f(g(x)) です。関数型プログラミングでは、小さな関数を合成して大きな処理を作る ことを推奨します。mapfilter をチェーンするのも一種の合成。compose を直接書けるようになると、関数を 値として扱う 感覚が身につきます。

本レッスンでは「整数 x を受け取り、x に対して addOne してから double を適用した結果を返す」関数を作ります。やりたいのは double(addOne(x)) です。中で compose を組み立ててから適用する形で書いてみましょう。

関数合成は データの流れの右から左 に読む。f ∘ g は「先に g、後で f」。 数学の式と一致する。

関数合成の例

addOne(x) = x + 1double(x) = x * 2 とします。compose(double, addOne)(3) の値を考えると以下のとおりです。

  1. addOne(3)4
  2. double(4)8

答えは 8。図にすると次のようになります。

diagram (will load when visible)

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

  • 注意したいのは compose(f, g)順序 です

注意したいのは compose(f, g)順序 です。f ∘ g先に g を適用してから f を適用します。逆に書くと意味が反転して結果が変わります。

Python での実装

Python では関数を として扱えるので、compose を関数として定義できます。

Python

def compose(f, g): return lambda x: f(g(x)) def addOneThenDouble(x): double = lambda n: n * 2 addOne = lambda n: n + 1 combined = compose(double, addOne) return combined(x)

compose関数を返す関数 です。lambda x: f(g(x)) の部分が新しい関数。これを combined に保存して combined(x) で呼び出します。

JavaScript での実装

JavaScript

function addOneThenDouble(x) { const compose = (f, g) => (x) => f(g(x)); const double = (n) => n * 2; const addOne = (n) => n + 1; const combined = compose(double, addOne); return combined(x); }

JS でもアロー関数で短く書けます。compose(double, addOne) の戻り値は 新しい関数 で、それを (x) で呼び出します。

なぜ compose が便利か

単発で使うなら double(addOne(x)) と書けば済みます。compose が威力を発揮するのは 配列処理のチェーンパイプライン を組むときです。

Python

pipeline = compose(double, compose(addOne, square))

このように複数の関数をネストして合成すれば、データ変換の流れをひと続きで表現できます。Lodash の flow や Ramda の compose、RxJS の pipe などは、すべてこの考え方の発展形です。

関数合成は 抽象度の高い再利用 を可能にする。小さな関数を組み合わせるだけで、複雑な処理ができる。

関数を値として扱う感覚

プログラミング初学者にとって、関数を データのように受け渡す のは最初は不思議に感じるかもしれません。けれど、整数 と同じく 関数 も値です。配列に詰めたり、変数に代入したり、別の関数の引数として渡したりできます。これを 高階関数 (higher-order function) と呼びます。composemapfilterreduce も、すべて高階関数の仲間です。

よくある間違い

1 つ目は 適用順序の取り違えcompose(f, g)f(g(x)) であって g(f(x)) ではありません。逆順で適用したいときは pipecompose(g, f) と書きます。2 つ目は 関数を即座に呼んでしまう こと。compose(double(), addOne()) のように () を付けると関数ではなく値を渡すことになりエラー。compose(double, addOne) のように関数オブジェクトのまま渡します。3 つ目は 戻り値を呼び出し忘れる こと。compose(...) は関数を返すので、compose(...)(x) のように呼び出さないと結果が出ません。

やってみよう

  • doubleaddOne の順序を入れ替えて、addOne(double(x)) を返す関数を作ってみる。x = 37 になるか確認。
  • 3 つの関数を合成してみる。square(double(addOne(x))) のように。
  • compose を使わず pipe 版を書いてみる。pipe(f, g)(x) = g(f(x))

関数を 値として扱える 言語は強力。map filter reduce compose を組み合わせれば、ループや条件分岐より簡潔に書ける場面が多い。

よくある質問

Q. 高階関数とは何ですか?

A. 関数を引数に取る、または関数を返す関数のことです。map / filter / reduce が代表で、コールバックを受け取って汎用化された処理を提供します。「処理の差分だけ渡せる」ため、ボイラープレートが減りコードが宣言的になります。

Q. カリー化はどんな場面で使いますか?

A. 引数を順番に部分適用したいときや、関数合成を綺麗に書きたいときに有効です。例えば add(a)(b) と書ければ add(5) で「5 を足す関数」が作れます。React のイベントハンドラ生成や、設定付きユーティリティ関数を作るのに便利です。

Q. 関数型と OOP はどっちを使うべき?

A. 状態を持つドメインモデルは OOP、データ変換パイプラインは関数型と使い分けるのが現代的です。Java も Stream で関数型を取り入れており、JS も class と高階関数を併用します。完全にどちらかに振らず、適材適所で組み合わせる設計が主流です。

次のレッスン

次は カリー化 で、カリー化 を学びます。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

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

理解度チェック (30 秒)

Q. 関数合成 とは何か、1 文で説明してください。

この章のポイント

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

関連レッスン

要件

  1. compose(f, g)(x) = f(g(x)) のように関数を組み立てる処理を内部に書くこと
  2. addOne(n) = n + 1, double(n) = n * 2 として、double を後に適用した結果を返す
  3. 戻り値は整数 (int)。式は (x + 1) * 2 と等価

入出力例

test-cases.txt

addOneThenDouble(3)8 addOneThenDouble(0)2 addOneThenDouble(5)12 addOneThenDouble(-1)0 addOneThenDouble(10)22 addOneThenDouble(-5)-8

ヒント

main.py
main.py
学習モード

メモ

関数合成

⌘S で保存