スポンサーLobeHubLobeHub詳しく見る
dshfind

第8課:リアクティブ余効果:依存が揃ったら自動起動

一言でいうと:リアクティブ余効果とは「自動的に変化する依存テーブル」のことです——コンポーネントは自分が何を必要とするかを宣言するだけで、システムがそのテーブルを監視し、依存が揃った瞬間にコンポーネントを自動的に起動(Reload)し、依存が欠けた瞬間にコンポーネントを自動的にアンロード(Unload)します。順序やタイミングを気にする必要はまったくありません。


1. 余効果コンテキスト:「キー → 型付きの値」の依存テーブル

前課ではロールバック可能なエフェクト——「途中で取り消せる」ことを解決する仕組み——を学びました。しかし、コンポーネント同士がどうやって互いに依存するかはまだ解決していません。この課でその部分を補います。

伝統的な答えは IoC コンテナ(制御反転コンテナ) です。素のキー・バリューマップで、"database" → データベースオブジェクトを登録し、他のコンポーネントが名前で取り出します。Cordis の論文はこれを型付きの依存テーブルへとアップグレードし、余効果コンテキストと呼んでいます:

Σ ≔ (𝑘 : 𝐾) ⇀ 𝒱𝑘

読み方:あらゆる種類の「依存キー」k(キー集合 K に属する)に対して、テーブルには型 𝒱𝑘 の値を格納できます。あの記号は部分関数であることに注意してください(⇀ は部分を表します):テーブルにはあるキーが存在しなくてもよいのです。平易な言葉で分解すると:

成分何か
キー k依存の名前"database""translator"
依存そのものデータベース接続、翻訳エンジンオブジェクト
型族 𝒱「キー → そのキーの値の型」の対応表キー "database" の値はデータベース接続型でなければならない

𝒱 があることで、各キーは自分の値の型に静的に束縛されます——これが素のキー・バリューテーブルより強い点です。依存アクセスに静的な型安全性があるため、型を取り違えると実行途中で爆発するのではなく、コンパイル時にエラーになります。

テーブルには4つの記法があります(論文の定義12):

  • σ(k):参照——キー k がテーブルにあるときの値;
  • σ[k ↦ v]:挿入——キー k、値 v をテーブルに追加する(前提:k がそれ以前テーブルにないこと);
  • σ ∖ k:削除——キー k をテーブルから取り除く(前提:k がもともとテーブルにあること);
  • k ∈ dom(σ)k がテーブルにあるかどうか。

2つの前提条件に注目してください:「二度挿入できない」「存在しないものは削除できない」。これはまさにロールバック可能なエフェクトにおける冪等性の要求に対応します——同じことを二度行ってはいけないからこそ、取り消しが意味をなすのです。

2つの中核操作(論文の定義13):

  • get(k):テーブルからキー k の値を取り出す(前提:k がテーブルにあること。さもなければ実行時に失敗);
  • set(k, v):キー k、値 v をテーブルに入れる(前提:k がテーブルにないこと)。「新しいテーブル + 取り消し関数」を返し、取り消し関数が k をテーブルから取り除く役目を負います。

ここにこの課全体で最も重要な洞察が隠れています。set 自体が余効果コンテキスト上のエフェクト関数なのです。したがって、前課のエフェクト機構をそのまま使えます——システムは依存の登録を自動的に追跡し、必要なときに自動的に登録を取り消します。余効果操作はエフェクトであり、エフェクトはロールバック可能です。一方が「必要に応じてインストールする」を担当し、もう一方が「取り壊せる」を担当し、両者はぴったりと噛み合います。

IoC コンテナと比較してみましょう:

IoC コンテナ余効果コンテキスト Σ違いはどこか
素のキー・バリューマップ依存の部分関数型族 𝒱 があり、静的な型安全性がある
手動登録・手動インジェクションset / getset はエフェクト関数であり、自動的に取り消せる
依存が欠けるとエラー依存が揃うのを待ってからアクティブ化楽観的アクセスをしない(次節で説明)

2. 仕様と充足性:全部揃って初めて意味をなす

このテーブルがあれば、コンポーネントは「準備ができた」をどう表現するのでしょうか? 答えは、依存仕様 d を宣言することです——「私はこれらのキーを使う」ことを意味するキーの集合です。

システムがコンポーネントを起動できるかどうかを判断するのに頼るのは、充足述語です:

σ ⊨ d  が成り立つのは  d のすべてのキーが dom(σ) にある場合、かつその場合に限る

数式で書けば σ ⊨ d ≔ ∀ k ∈ d. k ∈ dom(σ) です。平易に言えば、1つでも欠けていれば充足しない——これは「かつ」の関係であって「または」ではなく、全部揃って初めて意味をなします

この課全体を貫く例を挙げましょう。ある翻訳プラグインが仕事をするには2つのものが必要です——"database"(語彙を引く)と "translator"(翻訳エンジン)。これが宣言する仕様は:

d = { "database", "translator" }

テーブルの変化とプラグインの状態:

テーブルにあるもの充足するか翻訳プラグイン
"database" のみ充足しない(translator が欠けている)起動しない
"database""translator" が両方ある充足する起動して動作
"translator" はまだあるが "database" が取り除かれた充足しない(database が欠けている)停止

なぜこんなに厳格なのでしょうか? 論文は率直に述べています。コンポーネントは依存に楽観的にアクセスすべきではない——存在しない依存へのアクセスは実行時に失敗します。正しい姿勢は「アクセスしながら爆発する」ではなく、「すべての依存が揃ってからアクティブ化する」です。

この判断が本当に可能であることを保証する技術的ポイントがもう2つあります:

  • 決定可能dom(σ) は有限集合なので、「充足するかどうか」は必ず計算でき、止まることはありません;
  • すべての変化が観測される:テーブルへのすべての変更はエフェクト関数を経由して行われ(これらの関数の逆は以前の定義域を復元します)、すべてのエフェクト境界での充足性の変化が検出できる——これが「リアクティブ性」の代数的基礎です。システムはすべての余効果の変化が観測されることを保証します。

3. 通知と分類:すべての変化をアクティブ化型・停止型・中立に分ける

今、システムはテーブル σ を持ち、各コンポーネントは仕様 d を持っています。ここからの規則は単純です。テーブルが変わるたびに、「変化前の σ」と「変化後の σ′」を比較し、d の充足性が変わったかどうかで分類します(論文の定義15):

分類条件意味システムの動作
アクティブ化型前は充足せず、後は充足依存がちょうど揃ったコンポーネントのエフェクトを実行(完全に追跡)→ Reload
停止型前は充足し、後は充足しない依存がちょうど欠けた累積したエフェクトを復元 → Unload
中立その他(両側とも充足、または両側とも非充足)充足性は変わらない起動も停止もしない(値が変わればリロード)

擬似コードで書けば、論文のあの3分岐の判定です:

if      前は充足せず かつ 後は充足する  →  アクティブ化型
else if 前は充足し かつ 後は充足しない  →  停止型
else                                 →  中立

3つの重要ポイント:

① アクティブ化は Reload をトリガーし、停止は Unload をトリガーします。 アクティブ化型の遷移は依存がちょうど揃ったことを意味し、システムはコンポーネントのエフェクトを実行して完全に追跡し、コンポーネントが動き始めます。停止型の遷移は依存がちょうど欠けたことを意味し、システムはそれまでに累積したすべてのエフェクトを復元します——コンポーネントはクリーンにアンロードされ、副作用は一切残りません。では中立の遷移は? 例えば「値は変わったが依存は依然揃っている」——充足性は変わっていないので起動も停止もしませんが、システムはリロード(reload)を一度トリガーし、コンポーネントが新しい値を受け取れるようにします。

② 依存順序は自動的に生まれ、手動で宣言する必要はありません。 コンポーネント A が set("database", ...) でキーを提供し、コンポーネント B が "database" ∈ d_B を宣言するとします。すると:

  • B の充足性は "database" ∈ dom(σ) を要求し、"database" は A が完全にアクティブ化され、set が実際に効果を発して初めてテーブルに現れます——だから B は A の後にアクティブ化します(依存する側は依存先の後にアクティブ化する);
  • 逆に、A をアンロードすると "database" がテーブルから取り除かれ、B の充足性は瞬時に壊れます——だからシステムは A が復元を始める前に B が完全に停止していることを保証します(依存先は依存する側の後にアンロードされる)。

この順序は誰かが宣言する必要がありません。通知機構の自然な帰結です。論文の原話では、正しい依存順序は構造的保証になる——順序はシステムが導出するので、書き間違えることができません。

③ コンポーネント自身は何も管理しません。 起動・待機・停止はすべてシステムが充足性を監視して自動的に行い、コンポーネントが唯一すべきことは「何が必要か」を宣言することだけです。下の図がこの一連の流れです:

等待依赖不满足(缺东西)不启动,不报错依赖齐了 → 激活激活(运行)执行组件效应,自动跟踪依赖满足(都齐了)依赖没了 → 停用停用撤销全部副作用系统盯着变化,自动激活 / 停用——组件自己不用管值变了但依赖仍齐 → 自动重启(重载)

依赖满足性变化 → 激活 / 停用 / 中性 三种迁移分类

図の流れは、待機(何かが欠けている)→ 依存が揃う → アクティブ化(実行中)→ 依存がなくなる → 停止(すべての副作用を取り消す) です。下部の「値は変わったが依存は依然揃っている → 自動再起動(リロード)」という一文は、まさに中立遷移におけるリロード動作に対応します。

4. 分離とインターセプション:同じテーブルの2つの高度な使い方

基本コンテキスト Σ はすべてのコンポーネントが共有するフラットなテーブルです。実際のシステムではこれでは足りないことが多く、論文は2つの拡張方向——分離(Isolation)インターセプション(Interception)——を示しています。これらはまったく異なる問題を解決します。

4.1 分離(Isolation):同じキーが異なるコンテキストで異なる値に解決される

まずシナリオを見ましょう。マルチテナントシステムで、テナント A とテナント B の両方が "database" というキーを使いたいが、それぞれのデータベースに接続しなければなりません。フラットな1枚のテーブルではこれはできません。

解法はテーブルを2層に分けることです:

Σiso = 分離ドメインテーブル ρ(キー → ドメイン識別子 r) × 依存テーブル σ(ドメイン識別子 r → 型付きの値)

キー k にアクセスするとき、まず ρ(k) を引いてドメイン識別子 r を得て、次に σ(r) を引いて実際の値を得ます。操作がもう1つ増えます。isolate(k, r):キー k をドメイン r に束縛します。これにより:

  • 同じキー k が、異なる分離ドメインでまったく異なる値に解決される
  • 分離は実行時に動的に調整できる——従来の依存性注入よりも細かい粒度で、特定のコンポーネント向けに個別にカスタマイズできます;
  • すべての操作は依然としてエフェクト関数(𝔈Σiso)であり、変わらずロールバック可能です。

論文はこれを「実行時アドホック多相システム」と呼んでいます。これはマルチテナントシステム、テスト環境(テストケースごとに1つの分離ドメインで、相互に汚染しない)、コンポーネントのサンドボックスに広く適用できます。

4.2 インターセプション(Interception):依存値を変えずに横断的メタデータを付加する

別のシナリオを見てみましょう。依存値を差し替えたいのではなく、アクセス時に少しの追加情報を付けたい——例えば、すべてのデータベースアクセスに「現在のユーザーが誰か」を付けたり、ログタグや権限マークを追加したりする場合です。

解法はテーブルにメタデータ層を加えることです:

Σinter = コンテキストメタデータ ι(コンテキストが保持) × プロバイダ関数テーブル σ(キー → 「メタデータ → 値」の関数)

キー k にアクセスするとき、システムはコンポーネントが宣言したメタデータ d(k)コンテキストが保持するメタデータ ι(k)マージし(各キーには独自のマージ意味論があり、例えばスカラーフィールドは右側の値を取り、コレクションフィールドは和集合を取る)、プロバイダ関数をマージ結果に適用します。マージは右優先であることに注意してください。コンテキストのメタデータに優先権があり、コンポーネントの宣言を上書きできます。これにより、外側のコンテキストはコンポーネント自体を変更することなく、コンポーネントが余効果をどう使うかを制約できます。

分離 vs インターセプション、一言で見分ける

分離 Σisoインターセプション Σinter
解決する問題同じキーが異なるコンテキストで異なる値に解決されるアクセス時に追加の振る舞い / メタデータを付加する
実現手段キー → ドメイン → 値、二層マッピングメタデータのマージ + プロバイダ関数
たとえ各テナントが自分のデータベース接続を使うすべてのアクセスに自動的に「現在のユーザー」タグが付く
依存値は変わるか変わる(別の値に差し替わる)変わらない(同じ値で、情報が付加されるだけ)

キーポイントの振り返り

  1. 余効果コンテキスト Σ は「キー → 型付きの値」の依存テーブルset で入れ、get で取り、取り消し可能。型族 𝒱 が静的安全性を保証し、IoC コンテナの素のキー・バリューテーブルより強い。
  2. 充足述語 σ ⊨ d:d のすべてのキーがテーブルにあること——1つでも欠ければ充足せず、全部揃って初めて意味をなす。
  3. すべてのテーブル変化は充足性が変わったかどうかで分類される:アクティブ化型 → Reload、停止型 → Unload、中立 → 起動も停止もしない(値が変わればリロード)。
  4. 依存順序は構造的保証であり、手動宣言は不要:依存する側は依存先のアクティブ化の後にアクティブ化し、依存先は依存する側が停止した後にアンロードされる。
  5. 分離 = 同じキーが異なるコンテキストで異なる値に解決される(マルチテナント / テスト)。インターセプション = 横断的メタデータを付加し、依存値は変えない

🚀 次課ではカメラを近づけて、単一コンポーネントの一生を見ます。第9課「コンポーネントのライフサイクル:冪等性・反復・エポック・非同期」。

セルフテスト · リアクティブ余効果

回答を終えたら「解答を送信」をクリックすると、正誤と解説を確認できます。

1. 翻訳プラグインが仕様 d = {"database", "translator"} を宣言し、現在の依存テーブル σ には "database" しかありません。σ ⊨ d について正しいのはどれですか?
2. あるコンポーネントの仕様 d はずっと充足しており、今回のテーブル変化後も依然として充足していますが、ある依存の値が変わりました。この遷移はどう分類すべきですか?
3. コンポーネント A が set("database", ...) で依存を提供し、コンポーネント B が d_B = {"database"} を宣言しています。A と B の起動・停止の順序について正しいのはどれですか?
4. マルチテナントシステムで、テナント A とテナント B の両方が "database" というキーを使うが、それぞれ自分のデータベースに接続しなければなりません。これはどのメカニズムで解決すべきですか?