第 10 課:コンテキストパラダイム:コンテキスト型の統一
ひとこと要約:コンテキストパラダイム(Context Paradigm)は、「私はどこにいるか(現在の状態)、私は何を変えたか(逆関数)、私は何を必要とするか(依存テーブル)」という 3 つのものをすべて、同じ再帰可能な ctx エンティティに詰め込みます。これにより、コンポーネントの「プラグイン・プラグアウト」は単なる比喩から、実際に実装できる構造へと変わります。アンドゥと再接続の正しさは、もはや開発者の自律ではなく、構造そのものによって保証されます。
ステップ 1:1 つの ctx に 3 つのものを入れる——「私はどこにいるか、何を変えたか、何を必要とするか」
まず、前のレッスンまでの 2 つの主軸を思い出しましょう。
- エフェクト(effect):コンポーネントが動作するときに残す副作用(ファイルの変更、プロセスの起動……)。アンドゥできることが求められます。
- コエフェクト(coeffect):コンポーネントが動作するときに必要とするもの(あるサービス、ある設定……)。自動で接続できることが求められます。
これまで、私たちはこれらを 2 つの別々のメカニズムで処理してきました。この節の論文は大胆な統一を行います。エフェクト、コエフェクト、そして「現在の状態」をすべて同じエンティティに詰め込むのです。それが再帰的なコンテキスト型です。
定義 24(コンテキスト型):Γ∞ ≔ μΓ. Γ × (Γ → Γ) × Σ
この式が読めなくても大丈夫です。分解してみると、要するに一言:1 つの ctx の中に 3 つのものが同時に入っている、ということです。
| 成分 | 入っているもの | 平易な言い方 | ひとこと例え |
|---|---|---|---|
| Γ | 現在のコンテキスト状態(再帰的) | 私は今どこにいるか、世界が今どう見えるか | 現在位置 |
| Γ → Γ | 累積逆関数 | 私がこれまで何を変えたか、どう一歩ずつ戻るか | アンドゥログ |
| Σ | 依存テーブル | 私が今必要としているもの | 買い物リスト |
3 つの成分を合わせると、コンポーネントが環境とやり取りする前に毎回答えなければならない 3 つの質問になります。私はどこにいるか? 私は何を変えたか? 私にはまだ何が足りないか? すべてのやり取りは、この 1 つの ctx エンティティだけを通ります。
一个 ctx 同时记住「我在哪、我改了什么、我需要什么」——效应与余效应合体
なぜ「再帰」と呼ぶのでしょうか? それは、1 つ目の成分 Γ の中に入っているのも、やはり同じ Γ だからです。マトリョーシカのように、どの層も同じ種類のものなので、どれだけ深くネストしても構いません。この「自分が自分を内包する」構造には専門の呼び名があります。自己相似性です。論文の前半ではエフェクトをどんどん積み上がる「𝜕 タワー」として抽象化していましたが、ここでは再帰を使ってタワー全体を 1 つの型に平坦化しています。
さらにもう一段深く見ると、この設計には 2 つの追加の利点があります。
- エフェクトが ctx 上の自己準同型(エンドモルフィズム)になる:1 回のエフェクトとは「ctx を 1 つ持って入り、新しい ctx と 1 つの逆関数を持って出る」こと。出入りは同じ型なので、どんなエフェクトでも任意に連結できます。
- Σ はすべての共有状態を入れられる:Σ の基底となる型に制限がないため、コンポーネント間で共有したいグローバル状態はすべて、Σ の中の型付き依存としてエンコードできます。つまり、Σ がカバーするのは「コンポーネント間の依存」だけでなく、すべての共有可変状態です。
ステップ 2:階層的な合成——プラグイン・プラグアウトは本当に「挿す」と「抜く」だけ
前のレッスン(コンポーネントのライフサイクル)で「コンポーネントはプラグのように着脱できる」と述べました。Γ∞ ができた今、この比喩はついにレトリックから構造へと変わります。
ctx は再帰的なので、子コンポーネントの ctx は自然に親コンポーネントの ctx の中にネストされます。親コンテキストが複数の子階層のエフェクトを集約し、木状の制御構造を形成します。
┌───────────────┐
│ 親コンテキスト │ ← すべての子コンポーネントのエフェクトを集約・管理
└───────┬───────┘
┌───────────┼───────────┐
▼ ▼ ▼
[コンポーネント A] [コンポーネント B] [コンポーネント C] ← それぞれ独立に着脱でき、互いに影響しない
「プラグイン・プラグアウト」は 2 つの操作に直接対応します。
| 操作 | 何をするか | 平易な言い方 |
|---|---|---|
| コンポーネントのマウント | そのエフェクトを実行する(挿入) | プラグを挿して、効果が有効になる |
| コンポーネントのアンマウント | そのエフェクトを復元する(抜出) | プラグを抜いて、効果が取り消される |
この設計には 3 つの重要な保証があります。
- 抜いても他に影響しない:コンポーネントをアンマウントしても復元されるのはそのコンポーネント自身のエフェクトだけであり、実行中の他のコンポーネントには一切影響しません。
- 階層同士が干渉しない:木の異なる階層にあるコンポーネントは、それぞれ独立にマウント・アンマウントでき、グローバルに統一された順序は不要です。
- 任意の深さでネストできる:親コンテキストはすべての子コンポーネントのエフェクトを集約・管理し、子コンポーネントはさらに子コンポーネントを内包できます。何層でも好きなだけ重ねられます。
🎁 例えるなら:親コンテキストは「複数の差込口のある電源タップ」のようなものです。1 台の機器を抜いても、他の機器には変わらず電力が供給されます。しかも電源タップ自体を別の電源タップに挿して、無限に延長できます。
ステップ 3:同じエフェクトに 2 つの実装——インプレースか派生か
論文はここで非常に重要な概念の切り分けを行います。表示(denotation)と実装(implementation)の分離です。
ある操作を Γ∞ 上のエフェクトとして型付けするとき、固定されるのはその表示——「1 つの後続状態 + 対応する 1 つの逆関数」です。しかし実装は固定されません——その逆関数を実際にどう実行するかは、実装側に委ねられます。
定義 25(エフェクト関数の 2 つの実装):エフェクト関数 f には 2 つの実装方式が許されます。
| インプレース実装(in-place) | 派生実装(derived) | |
|---|---|---|
| コンテキスト | 元のコンテキストを変更し、後続状態は入力のエイリアスになる | 入力を変更せず、再帰構造の中で新しいコンテキストを返す |
| 逆関数 | 非自明な逆関数を返す(変更を実際に記録する) | 恒等関数を返す(何も変えていないので、アンドゥの必要がない) |
| 復元方法 | 逆関数を実行して、直前の変更を逆転する | 派生した新しいコンテキストを破棄する |
| 例え | 契約書の原本に直接変更を加え、毎回の変更をアンドゥリストに記録し、戻したいときはリストを逆順にたどって修正する | コピーを 1 部取ってコピーだけを変更し、原本には触れない。不要になったらコピーをシュレッダーにかける |
「表示」と「実装」の区別に注意してください。同じエフェクトでも表示は固定(後続状態 + 逆関数)ですが、2 つの実装のどちらかを選べます。どちらを選ぶかはホスト環境次第です。
- 純粋関数型環境では「インプレース変更」というものが存在しないため、2 つの実装は一致します(どちらも派生しかできません)。
- 命令型ホストでは、開発者は操作ごとに自由に選べます。速く変更したければインプレースを、元のデータに触れたくなければ派生を使います。
💡 論文では第 4.1.2 節で 2 つの実装の代表的な書き方が示されると述べています。次のレッスンで、それらがどのような姿かを見ていきます。
ステップ 4:なぜこれが 1 つの「パラダイム」と言えるのか?——2 つの旧来の考え方との対比
論文の野心は「1 つの型を与える」ことにとどまらず、このコンテキスト型それ自体が 1 つのプログラミングパラダイムを構成すると主張しています。これを理解するには、まず 2 つの旧来のパラダイムが副作用をどう扱ってきたかを見ましょう。両者は同じスペクトルの両極に立っています。
左端:明示的な状態受け渡し(関数型)
純粋関数型言語は参照透過性を保つため、副作用を状態への明示的な変換としてモデル化します。典型的な例は状態モナド S → (A, S) で、環境をすべての計算に貫通させます。
- ✅ 利点:エフェクトが型に見えるので、等式推論が可能で、追跡可能性が非常に高い。
- ❌ 代償:呼び出しチェーンのすべての関数が状態パラメータを受け取り返さなければなりません。ただ状態をそのまま受け渡すだけでも同じです。エフェクトの次元が増えると(ログ、設定、I/O)、モナドスタックやエフェクトハンドラのボイラープレートが急速に膨張します。
🎁 例えるなら:会社が「すべての書類はすべての社員の手を経て、全員の署名を取る」というルールを定めたようなものです。ただ転送するだけでも同様です。追跡はできるが、疲れ果てます。
右端:暗黙的な変更(命令型 / OOP)
主流の命令型言語では、コンポーネントが共有状態を直接変更し、依存を直接取り出すことができ、呼び出し箇所では何も宣言する必要がありません。
- ✅ 利点:書くのが楽で、エルゴノミクスに優れる。
- ❌ 代償:追跡不可能。論文は 2 つの生々しい例を挙げています。
- エフェクト側:React の
useEffectフック——コンポーネント内部のファイバー上に永続的な副作用を登録しますが、エフェクトの対象も登録メカニズムも明示的なパラメータではなく、その同一性はランタイム状態に隠れた呼び出し順序の位置によって決まります。 - コエフェクト側:Java のサービスロケータ(例えば Spring の
getBean(...))——グローバルレジストリから依存を取得し、すべての呼び出し箇所で null チェックと型キャストが必要で、依存関係はコードベース全体に暗黙的に散在しています。
- エフェクト側:React の
さらに悪いことに、f() がシステムの何を変更し、何に依存しているかを理解するには、呼び出し関係をたどってその実装を再帰的に読む必要があります。リファクタリングも脆くなります。ある呼び出しを移動・削除するだけで、遠く離れた不変条件を音もなく壊すかもしれません。
🎁 例えるなら:共用の黒板で、誰でも書き込めるが誰も名乗らないようなものです。便利だが、問題が起きても誰が変えたのか分からない。
中間:コンテキストパラダイム——両方のいいとこ取り
コンテキストパラダイムはこの両極を縫い合わせます。エフェクトとコエフェクトの両方が、明示的なコンテキストパラメータを介して仲介されるのです。したがって:
- すべての操作は、それを呼び出した ctx に帰属させられ、さらにその ctx を所有するコンポーネントに帰属させられます。関数型のように追跡可能です。
- 開発者は呼び出しチェーンの中に状態パラメータを山ほど手書きする必要がありません。命令型のように簡潔です。
しかもこれは単なる「妥協」ではなく、正しさの保証をさらに強化しています。
| 場面 | 旧来のやり方(自律頼み) | コンテキストパラダイム(構造頼み) |
|---|---|---|
| ロールバック可能なエフェクト | すべての複合操作について、開発者が自分でアンドゥロジックを書く必要がある | 各アトミック操作に逆関数を与えるだけで、複合操作の逆関数は合成によって自動的に得られる。マウントと分解は構造上可逆になる |
| リアクティブなコエフェクト | 依存が正しく接続されるかどうかは、注意力次第 | コンポーネントは必要な依存を宣言するだけで、ランタイムが自動的に解決し再接続する。プロバイダが追加・削除・置き換えられても、接続は常に正しく保たれる |
2 つの方向を合わせると、この節の核心的な結論になります。これまで開発者の自律によって保証する必要があった正しさが、このパラダイムの構造的性質になったのです。
要点の振り返り
情報量は少なくありませんでしたが、次の 5 文だけ覚えておけば十分です。
- 統一コンテキスト:Γ∞ ≔ μΓ. Γ × (Γ → Γ) × Σ——1 つの ctx が「現在の状態 Γ」「累積逆関数 Γ → Γ」「依存テーブル Σ」を同時に保持する。つまり「私はどこにいるか、何を変えたか、何を必要とするか」。
- 再帰的な自己相似性:Γ の中に入っているのも Γ で、マトリョーシカのようにどれだけ深くネストしてもよい。エフェクトは ctx 上の自己準同型(ctx が入り、ctx と逆関数が出る)になり、Σ はすべての共有可変状態をエンコードできる。
- 階層的な合成:親コンテキストは子のエフェクトを集約して木構造を形成する。マウント = エフェクトの挿入、アンマウント = 抜出と復元。互いに影響せず、任意の深さのネストをサポートする。
- 表示と実装の分離:エフェクトは「表示」(後続状態 + 逆関数)を固定し、実装は固定しない。インプレース実装(ctx を変更 + 非自明な逆)または派生実装(新しい ctx を返す + 恒等逆、復元 = 破棄)。
- パラダイムとしての位置付け:関数型の明示的状態受け渡しは追跡可能だがボイラープレートが多く、命令型の暗黙的変更は使いやすいが追跡不可能。コンテキストパラダイムは明示的な ctx で両方を兼ね備え、正しさを「開発者の自律」から「構造的保証」へと変える。
🚀 机上の議論はここまでです。次のレッスンは論文の第 4 章「実装とケーススタディ」に入ります。Cordis コアライブラリで、Γ∞ がどのように実際のコードになり、インプレース / 派生の 2 つの実装が実際にどう書かれるのかを見ていきます。
セルフチェック · コンテキストパラダイム
回答を終えたら「解答を送信」をクリックすると、正誤と解説を確認できます。
