関数合成
関数合成
このレッスンで分かること
- 関数合成は 抽象度の高い再利用 を可能にする
- 関数合成 は数学にも出てくる基本概念で、2 つの関数
fとgからf(g(x))を作る操作を指しますaddOne(x) = x + 1、double(x) = x * 2とします
関数合成 とは
compose(f, g)(x) = f(g(x))の関数合成を関数の中で組み立てて適用する。addOneとdoubleを合成して値に適用した結果を返す関数を実装する。
関数合成 は数学にも出てくる基本概念で、2 つの関数 f と g から f(g(x)) を作る操作を指します。記号で書くと (f ∘ g)(x) = f(g(x)) です。関数型プログラミングでは、小さな関数を合成して大きな処理を作る ことを推奨します。map や filter をチェーンするのも一種の合成。compose を直接書けるようになると、関数を 値として扱う 感覚が身につきます。
本レッスンでは「整数 x を受け取り、x に対して addOne してから double を適用した結果を返す」関数を作ります。やりたいのは double(addOne(x)) です。中で compose を組み立ててから適用する形で書いてみましょう。
関数合成は データの流れの右から左 に読む。
f ∘ gは「先に g、後で f」。 数学の式と一致する。
関数合成の例
addOne(x) = x + 1、double(x) = x * 2 とします。compose(double, addOne)(3) の値を考えると以下のとおりです。
addOne(3)→4double(4)→8
答えは 8。図にすると次のようになります。
図のポイント (テキスト併記)
- 注意したいのは
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) と呼びます。compose も map も filter も reduce も、すべて高階関数の仲間です。
よくある間違い
1 つ目は 適用順序の取り違え。compose(f, g) は f(g(x)) であって g(f(x)) ではありません。逆順で適用したいときは pipe や compose(g, f) と書きます。2 つ目は 関数を即座に呼んでしまう こと。compose(double(), addOne()) のように () を付けると関数ではなく値を渡すことになりエラー。compose(double, addOne) のように関数オブジェクトのまま渡します。3 つ目は 戻り値を呼び出し忘れる こと。compose(...) は関数を返すので、compose(...)(x) のように呼び出さないと結果が出ません。
やってみよう
doubleとaddOneの順序を入れ替えて、addOne(double(x))を返す関数を作ってみる。x = 3で7になるか確認。- 3 つの関数を合成してみる。
square(double(addOne(x)))のように。 composeを使わずpipe版を書いてみる。pipe(f, g)(x) = g(f(x))。
関数を
値として扱える言語は強力。mapfilterreducecomposeを組み合わせれば、ループや条件分岐より簡潔に書ける場面が多い。
よくある質問
Q. 高階関数とは何ですか?
A. 関数を引数に取る、または関数を返す関数のことです。map / filter / reduce が代表で、コールバックを受け取って汎用化された処理を提供します。「処理の差分だけ渡せる」ため、ボイラープレートが減りコードが宣言的になります。
Q. カリー化はどんな場面で使いますか?
A. 引数を順番に部分適用したいときや、関数合成を綺麗に書きたいときに有効です。例えば add(a)(b) と書ければ add(5) で「5 を足す関数」が作れます。React のイベントハンドラ生成や、設定付きユーティリティ関数を作るのに便利です。
Q. 関数型と OOP はどっちを使うべき?
A. 状態を持つドメインモデルは OOP、データ変換パイプラインは関数型と使い分けるのが現代的です。Java も Stream で関数型を取り入れており、JS も class と高階関数を併用します。完全にどちらかに振らず、適材適所で組み合わせる設計が主流です。
次のレッスン
次は カリー化 で、カリー化 を学びます。
事前確認 — 進む前に次の 3 つができることを確認しましょう。
- 関数合成 の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. 関数合成 とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
- compose(f, g)(x) = f(g(x)) のように関数を組み立てる処理を内部に書くこと
- addOne(n) = n + 1, double(n) = n * 2 として、double を後に適用した結果を返す
- 戻り値は整数 (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