1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Excelで「関数型脳」を作る Vol.3:関数を生み出す工場 —— クロージャと部分適用

1
Last updated at Posted at 2026-02-21

前回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)して計算します。
しかし、関数を入れ子にした場合、こんな動きをします。

  1. 外側の関数に 0.1 を渡す。
  2. その 0.1一時的にスタックに積まれる(Pushされる)が、まだ計算は走らない。 代わりに「スタックの状態を保持したまま、残りの引数を待つ関数」が返ってくる。
  3. 後から内側の関数に 1000 を渡す。
  4. 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のチャーチ数は、すでにカリー化の魔法を使っていた。
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?