前回Excelで「関数型脳」を作る Vol.2:関数は「値」だった —— 偶然のタイプミスとチャーチ数の衝撃で、偶然のタイプミスから「関数も値である」ことを学び、関数を名前(変数)に定義したり、引数として渡したりできることも学びました。
ということは、
「関数を引数として『渡せる(Input)』なら、逆に関数を戻り値として 『返せる(Output)』 ?!」と。
で、「関数を作り出す関数(Function Factory)」 が作れる?!と。
ということで、Vol.3のテーマは、「クロージャ(Closure)」と「部分適用(Partial Application)」 です。
ちょっと難しそうな名前ですが、要は 「便利な専用ツールを自動生成するテクニック」 のことです。
1. 命令型脳の最初の発想:「グローバル変数に代入」
例えば、消費税計算です。
- 「税抜価格」から「税込価格」を計算します。
- 税率には 10% と 8%(軽減税率)があります。
命令型脳の最初の発想では、次でした。
「関数の外に グローバル変数のような『現在の税率』 を定義して、そこに代入(セット)して使い回す」という発想で次のようなコードを考えました。
=LET(
// 1. まず現在の税率を定義する(グローバル変数的な発想)
_現在の税率, 0.1,
// 2. 計算関数は、外にある _現在の税率 を見に行く
_税込計算, LAMBDA(_価格, _価格 * (1 + _現在の税率)),
// 3. 引数1つで計算できた!よしよし!
_Aさん, _税込計算(1000),
// 4. さて、次は軽減税率(8%)のBさんだ。変数を書き換えよう…
_現在の税率, 0.08, // ← !?!?
_Bさん, _税込計算(500)
)
...いやいや、そもそも関数型の世界では「代入」できないじゃん!
Vol.1で「世界は書き換わらない( x = x + 1 の違和感)」というルールです。
Excelの LET 関数では、一度 _現在の税率 を 0.1 と定義(名前付け)したら、後から 0.08 に 再代入して書き換えることは絶対に不可能 なのです。
命令型なら、グローバル変数の値を書き換えながら同じ関数を使い回します、関数型プログラミングの世界ではできません。
2. 命令型脳の到達点:引数を2つにする
「再代入ができないなら、変化する部分は引数として受け取ればよいですね」
ということで、引数を2つに増やしました。
=LET(
_税込計算, LAMBDA(_価格, _税率, _価格 * (1 + _税率)),
_Aさん, _税込計算(1000, 0.1),
_Bさん, _税込計算(500, 0.08)
)
計算のロジックは1箇所にまとまったし、標準税率も軽減税率も両方対応できました。
命令型脳の到達点はここまでですが、関数型脳にはさらに先の世界があります。
Vol.2のチャーチ数で感じた、論理 と 物理 を分けたように、「設定」と「実行」を分離する という考え方です。
3. 「設定」を焼き付けた安全な部品を配る
引数2つでちゃんと動くのに、なぜ引数1つの「専用の道具」が必要になるのか?
それは、「設定」と「実行」を分離して、安全な部品として配りたい と思ったからです。
「設定」と「実行」を分離して、安全な部品として配りたい
例えば、この数式を現場の担当者に使ってもらうとします。_税込計算(価格, 税率) という関数を配布したとして、担当者は毎回「これは標準税率だから 0.1 を入力して...」と考える必要があり、誤って 0.10 ではなく 10 と入力するミスが起こりそうです。 0.1 は決まっているのだから、それ込みで配布したいです。
オブジェクト指向プログラミングであれば、TaxCalculator クラスを作って、そこに「税率= 0.1 」という 状態(クラス変数やプロパティ) を持たせたり、外部の定数ファイルから読み込ませたり(副作用)すれば、使う側は calc.execute(価格) と引数1つで安全に呼び出せます。
しかし、Excelの数式のような 純粋な関数型 の世界には、「状態を保持するオブジェクト」も「外部からこっそり値を読み込む副作用」も存在しません。すべては 「関数」と「引数」だけで解決しなければならない です。
「税率という 設定 は関数そのものに組み込んでおくから、あなたは価格の データ を入れるだけでいいよ」という、_標準税率で計算(価格) という安全なブラックボックス(部品)を作って渡したいという考えです。
そこで、手作業で「専用の道具(ラッパー関数)」を作ってみました。
=LET(
_税込計算, LAMBDA(_価格, _税率, _価格 * (1 + _税率)),
// 手作業で専用関数を作る
_標準税率で計算, LAMBDA(_価格, _税込計算(_価格, 0.1)),
_軽減税率で計算, LAMBDA(_価格, _税込計算(_価格, 0.08)),
// ...
)
これはこれでよいのですが、_税込計算(_価格, 0.1) の 0.1 の部分が気になります。ここを覚えさせたいんだよなぁ。。。そうすれば、安全に「標準税率で計算関数」「軽減税率で計算関数」を配布できるのに。。。
4. 本に書いてあった魔法の仕組み:「クロージャ」と「スタック」
「工場」を作るには、Vol.2で学んだ「関数も値として扱える」という考えを使えばよいのです。
関数型プログラミングの本を開くと、そのための 関数を入れ子 にした 「クロージャ(Closure:閉包)」 というという解決策が載っていました。
クロージャ と専門用語を聞くと「わからん」となりますが、これは、命令型脳の「関数呼び出し時のメモリのスタック(PushとPop)」の概念で考えれば、すごく普通のことでした。
普通の関数(引数2つの一括渡し)は、引数を全てスタックに積み(Push)、ポップ(Pop)して計算します。
しかし、関数を入れ子にした場合、こんな動きをします。
- 外側の関数に
0.1を渡す。 - その
0.1は一時的にスタックに積まれる(Pushされる)が、まだ計算は走らない。 代わりに「スタックの状態を保持したまま、残りの引数を待つ関数」が返ってくる。 - 後から内側の関数に
1000を渡す。 -
1000もスタックに積まれ、「必要な材料がすべて揃った!」となった瞬間に、スタックから一気にポップ(Pop)され、計算が実行される。
5. 工場の中で「専用の機械」を組み立てる
この仕組みを踏まえて、Excelのコードを書き直してみます。
「工場(外側の関数)」の中で、LET関数を使って「専用計算機(内側の関数)」を組み立て、それを外にリターンする構造です。
=LET(
// 1. 税率を受け取って、専用の計算機を作って返す「工場」
_税込計算工場, LAMBDA(_税率,
LET(
// ★ここがクロージャのメカニズム!★
// _税率(0.1)がスタックに積まれたまま「保留」された状態の関数を作る
// (ここなら _税率 がスコープ内にあるからエラーにならない!)
_専用計算機, LAMBDA(_価格, _価格 * (1 + _税率)),
// その「保留状態の関数」を値として外へ出荷(リターン)する
_専用計算機
)
),
// 2. 工場に税率を渡して、専用関数を作る(これを「部分適用」と呼ぶ)
// (この時点で、0.1がスタックにPushされ、次の入力を待っている)
_標準税率で計算, _税込計算工場(0.1), // 10%版の関数が完成!
_軽減税率で計算, _税込計算工場(0.08), // 8%版の関数が完成!
// 3. 現場では、専用関数を使うだけ!
// (ここで1000がPushされ、材料が揃ったので一気にPopして計算される!)
_結果, _標準税率で計算(1000), // ★ここ!劇的にシンプル!
_結果
)
動きました!
_税込計算工場(0.1) を実行したとき、工場の中では _税率 という名前に 0.1 が定義され、概念的なスタックにPushされます。
普通なら関数が終わるとスタックから消えてしまいますが、中に別の関数(_専用計算機)がいる場合、その内側の関数が「あ、この 0.1 は俺が後で使うから、スタックに残しておいて!」とホールド(閉包)してくれるのです。
このような、多引数の関数を「引数を1つずつスタックに積んでいく関数の連鎖」に変換することを、「カリー化(Currying)」 と呼びます。
6. すでにカリー化を使っていた
前回(Vol.2)で、チャーチ数(関数としての数)を作った時のコードです。
LAMBDA(f, LAMBDA(x, f(x)))
これ、すでにカリー化されてるじゃん!! よくわからないまま本を写経していたあの入れ子構造は、「関数 f をスタックにPushして保留し、次に x が来た瞬間にPopして実行する」というカリー化そのものでした。
7. まとめ:いよいよループ処理への準備が整った
「グローバル変数を書き換えよう」として挫折し、「引数2つで妥協」してオブジェクト指向の「状態」を恋しく思い、ついに 「汎用的なロジックから、専用の道具を自動生成する(クロージャ)」 という関数型ならではの強力な仕組みにたどり着きました。
- クロージャ: 関数が作られたときの環境(スタックの引数)をそのまま保留・記憶しておく 仕組み 。
- 部分適用: クロージャの仕組みを利用して設定値を固定し、使いやすい専用関数を作る テクニック(使い方) 。
- カリー化 複数の引数を取る関数を、1つずつスタックに積んでいく関数の連鎖(マトリョーシカ)にする 構造 。
これで、自分だけの「関数ライブラリ」を作れるようになりました。
さて、「引数が1つだけの専用関数」。
これが、Vol.4で紹介する「最強のデータ処理」に不可欠なピース なのです。
「1つずつの計算はできた。じゃあ、1万行のデータに対して、この『専用関数』を一気に適用したい ときはどうする?」
命令型脳なら、ここで for ループを使います。
しかし、関数型脳にループ変数は不要です。
次回、Excelで「関数型脳」を作る Vol.4:VBAのループが無くても大丈夫 —— フィルターして、行(レコード)で回す(FILTER / MAP / BYROW) です。
今日の気づき
- グローバル変数に「代入」して状態を変えるやり方は、関数型の世界では通用しない(Vol.1の教訓)。
- 純粋な関数型はオブジェクト指向のように「状態」や「副作用」を持てない。
- 代わりに、関数を入れ子にする(クロージャ)ことで、スタックに引数を積んだまま保留(ホールド)できる。
- この仕組み(カリー化)を使って「設定済みの専用関数」を作っておくと、後の作業が圧倒的に楽になる(部分適用)。
- Vol.2のチャーチ数は、すでにカリー化の魔法を使っていた。