Share
Embed
継続モナドとリアクティブプログラミング
リアクティブプログラミングには長い歴史があり、シンプルな emitter / listener から始まり Functional Reactive Programming (FRP), ReactiveX, Arrowised FRP, そして Solid.js, Vue, (React) の基礎になっています。
一方、継続モナドとは Haskell 等において制御構文的なものを実現するために使われるモナドトランスフォーマー(モナドを別のモナドに変換するもの)です。
このセッションではリアクティブプログラミング、継続モナドの解説と、実はリアクティブプログラミングが継続モナドで実現可能であり、そして、実はこの実装が Solid, React, Vue 等モダンなフロントエンドフレームワークと似た API を提供するという話をします。
また、非同期処理、リソース管理の話も併せて行います。
ゆきくらげ
July 12, 2026
Transcript
57 slides
-
内容 • リアクティブプログラミング • リソース • リソース指向リアクティブプログラミング (Quon) • 継続モナド
• Rules of Hooks と ArrowChoice • Agent ⽤ DSL (Katari) • Component Based な Agent Network 継続モナドとリアクティブプログラミング 背景 -
リアクティブ‧プログラミング 何? • → データの時間軸⽅向の変化を主軸とするパラダイム • データとそれに依存する処理を分けて書く 利点 • 更新伝播の
Source of Truth → 後述 使われている場所 • Webフロントエンド (React、Vue など) • ゲーム など 継続モナドとリアクティブプログラミング 背景 -
リアクティブ‧プログラミング let state = 0 state.subscribe(() { view.text =
state }) on("click", () { state = state + 1 }) 継続モナドとリアクティブプログラミング 背景 イベント駆動によってデータに依存する処理を切り出す let state = 0 updateView = () { view.text = state } on("click", () { state = state + 1 updateView() }) -
更新伝播の Source of Truth 継続モナドとリアクティブプログラミング 背景 Data Event Process Event
普通に書くとこうなる 値の更新 依存処理の実⾏(updateView) -
View 更新伝播の Source of Truth 継続モナドとリアクティブプログラミング 背景 Data Event Event
? View Process Data ‧Process をすべて実⾏するのは⾮効率。ではどの Process を実⾏する? ‧Event はそれぞれの Process がどのデータに依存しているか知らない Data や Process が複雑になると…… -
更新伝播の Source of Truth 継続モナドとリアクティブプログラミング 背景 Data Event Process Event
リアクティブプログラミングでは Process が Data を購読する 値の更新 購読 -
更新伝播の Source of Truth 継続モナドとリアクティブプログラミング 背景 Process Data Event Event
Process Process Data 購読 Processは、⾃⾝が依存する Data のみを Subscribe すればよい。 -
種類と特徴 • React Snapshot と差分適⽤ • Vue Reactive 値を明⽰的に扱う •
FRP Event 列と Behavior の分離 • Rx 実践的リアクティブプログラミングライブラリスタック 継続モナドとリアクティブプログラミング 背景 -
リソース リソースとは何か ‧メモリ上に確保されたデータ ‧WebSocket などの接続 ‧画⾯に表⽰されている UI ‧「event を購読している」という状態 →
"初期化" と "解放" 継続モナドとリアクティブプログラミング 背景 -
リソース安全性 リソース r が r' に依存しているとする リソース安全 = 任意の時点で、 「r
が確保されている → r' が確保されている」 継続モナドとリアクティブプログラミング 背景 A B C 依存 -
観察 継続モナドとリアクティブプログラミング 背景 リソース r が r' に依存している → r'
を r より前に解放してはいけない Rust とかは所有権でどうにかしている 発想: ⾃⾝の寿命を親に管理させる (つまり任意解放できない) 親側が⼦のリソースの初期化‧解放を管理 → これを時間軸⽅向に畳めば リアクティブプログラミングになりそう -
Routine 型 継続モナドとリアクティブプログラミング Quon type Routine t = Sync {result:
Async t, dispose: Async ()} Sync / Async は Haskell では IO 外側から、任意のタイミングで Sync を呼ぶ その後、Routine は内部でいくつかリソースを確保 確保したリソースの内⼀つを result に表出 Sync を呼んだ後は任意のタイミングで dispose を呼べる -
Routine 型 継続モナドとリアクティブプログラミング Quon type Routine t = Sync {result:
Async t, dispose: Async ()} time 初期化 result (t) dispose Routine t 外部 -
Routine 型の bind 継続モナドとリアクティブプログラミング Quon let program = do let
r1 = useR1(x) let r2 = useR2(r1) return resource2 初期化 解放 program useR1 useR2 Routine は解放処理を逆順に合成する 提供された Routine が Safe → 合成したものも必ず Safe -
Source 型 継続モナドとリアクティブプログラミング Quon type Source a = (a ->
Routine ()) -> Routine () 直感: 内部に a 型のリソースを複数持つ構造 時間で変化しうる リソースが追加された時リスナーを初期化 リソースが削除された時そのリスナーを解放 この subscribe 状態⾃体もリソース(外側の Routine) リスナー “Subscribeリソース”の確保 -
Source による Routine の階層 継続モナドとリアクティブプログラミング Quon type Source a =
(a -> Routine ()) -> Routine () リスナーは、いわゆる「コンポーネント」の初期化 Routine はモナドであるので、 リスナーを作る時内側でネストした購読を張れる リスナー -
階層とコンポーネントシステム ‧コンポーネント = その初期化処理と解放処理 = Routine 継続モナドとリアクティブプログラミング Quon Routine Routine
状態 購読 Routine Routine 親コンポーネントの Routine ⼦コンポーネントの Routine 群 -
継続モナド 突然ですが ContT r m a = (a -> m
r) -> m r と定めます 継続モナドとリアクティブプログラミング Quon -
継続モナド ContT r m a = (a -> m r)
-> m r a 型の値を少しリッチにしたもの 「後続の計算 a -> m r を受け取り m r を返す関数」 を a 型の値の代わりとして⽤いる。 継続モナドとリアクティブプログラミング Quon -
継続モナド ContT r m a = (a -> m r)
-> m r a 型の値は容易に Cont r m a へ変換できる (渡された関数 a -> m r を a に適⽤し、m r を返す) これは Monad における pure (return) になりそう 継続モナドとリアクティブプログラミング Quon -
継続モナド ContT r m a = (a -> m r)
-> m r Monad のもう⼀つの操作 Join : Cont r m (Cont r m a) -> Cont r m a つまり ((((a -> m r) -> m r) -> m r) -> m r) -> (a -> m r) -> m r はできる? 継続モナドとリアクティブプログラミング Quon -
継続モナド ((((a -> m r) -> m r) -> m
r) -> m r) -> (a -> m r) -> m r 継続モナドとリアクティブプログラミング Quon 1. m r を作らなければいけない 2. (a -> m r) -> m r から m r を作る処理があれば m r が得られる 3. (a -> m r) -> m r に (a -> m r) を適⽤すれば良い。 実は Monad 則を満たす[読者への課題]ので Monad ややこしいが、できる -
継続モナド 結局何ができる? reset : Monad m => ContT r m
r -> ContT r' m r shift : Monad m => ((a -> m r) -> ContT r m r) -> ContT r m a 継続モナドとリアクティブプログラミング Quon その時点での後続の計算 後続の計算を好きに加⼯して実⾏ -
継続モナドの使⽤例 リソースマネジメント 後続の計算 c = hdl を受け取った後の処理 shift により open
/ close で c を挟む → hdl の make / freeが⼀か所で書ける 継続モナドとリアクティブプログラミング Quon f = do hdl <- shift (\c -> hdl’ <- open c hdl’ close hdl’ ) content <- read hdl … -
継続モナドの使⽤例 ⼤域脱出 後続の計算を呼ばないことも可 すると⼤域脱出が可能 data が Invalid の場合短絡 継続モナドとリアクティブプログラミング Quon
f = do data fetch something t shift (\c case data of Invalid return “error” Valid text c text ) return $ “Hello” t その他にもいろんな制御ができる -
余談: Gleam の use はほぼ継続モナド 継続モナドとリアクティブプログラミング Quon // f :
fn(fn(a) r) r // g : fn(fn(b) s) s // に対して use x f use y g body → f(fn(x){ g(fn(y){ body }) }) に脱糖される use を使うことで、 その時点での継続を⼊⼿ 構⽂レベルで Cont モナドを実装 -
Source は継続モナド ContT r m a = (a -> m
r) -> m r なので‧‧‧ Source a = (a -> Routine ()) -> Routine () = ContT () Routine a 継続モナドとリアクティブプログラミング Quon -
Source は継続モナド 何故 ContT になるのか Cont は後続の処理のコントロールを可能にする Source の場合は ‧「後続処理」はリスナー
‧「コントロール」は動的なリソースの追加/削除 を表す 継続モナドとリアクティブプログラミング Quon -
継続モナドとしてSourceを使った場合 source = do let x = sourceX let y
= sourceY(x) f(x, y) 継続モナドとリアクティブプログラミング Quon ネスト構造をフラットに書ける 空間を張り合わせているように⾒える 利点: ⾯⽩い sourceX x x x y y y y f(x, y) f(x, y) f(x, y) f(x, y) sourceY sourceY sourceY -
Quon : 例 app = do tab useCell TabCounter
onEvent $ \case EvKey (KChar '2') [] set tab TabProfile EvKey (KChar '3') [] set tab TabTodos _ pure () body dyn loadingView $ cellSignal tab <&> \case TabProfile profileTab TabTodos todosTab pure $ body 継続モナドとリアクティブプログラミング Quon Tab 状態の作成 Event に対する反応 loadingView は React の Suspense みたいなもの Tab 状態の派⽣状態 (ここでは View) -
⽐較 この Routine の仕組みは Vue や Solid.js の setup function
に似ている ⼀⽅ Source のようなリソースの動的集合を主軸とするも のはあまりなさそう [要出典] Setup が Async にできるのは Async React っぽい 継続モナドとリアクティブプログラミング Quon -
形式化(おまけ) ℛ : Resources Γ, Δ ⊆ ℛ : Environment
r ↝ r′ : r depends to r' (ℛ : partial ordered by ↝) Σ = {init, dipose} ⨄ {7ill(r), dd} ⨄ {acq(r), rel(r)} : Events init, dispose: Input fill, dd: Output acq, rel: Internal 𝜎 ∈ Σ!" : Trace 𝑒 ∈ 𝜎 ↾ 𝑖: event e is evoked before time = i 𝜌# 𝑖 = {𝑟 ∈ ℛ: 𝘢𝘤𝘲 𝑟 ∈ 𝜎 ↾ 𝑖} ∖ {𝑟: 𝘳𝘦𝘭 𝑟 ∈ 𝜎 ↾ 𝑖}: Resources when t = i 継続モナドとリアクティブプログラミング Formalization -
形式化(おまけ) Under environment Γ,𝜎 ∶ Σ!" is admissible iff: -
(Uniq. resource) ack(r′) ∶ uniq. to r′, r′ ∉ Γ - (Init) 𝜎[0] = init - (Uniq. event) 7ill, dispose, dd, init ∶ up to 1 - (Fill consist.) if 𝜎[i] = 7ill(r), r ∈ 𝜌# 𝑖 - (Res. safety) if r ∈ 𝜌# 𝑖 , ∀r′ ↜ r, r′ ∈ 𝜌# 𝑖 - (Res. keep) until 7ill(r) to dispose, r ∈ 𝜌# t - (Disp.) if 𝜎[i] = dd, i is 7inal elem, and 𝜌# i = ∅ 継続モナドとリアクティブプログラミング Formalization -
形式化(おまけ) 継続モナドとリアクティブプログラミング Formalization Definition of Routine Set M ⊆ Σ!",
Γ ⊢ Routine(M) iff (1) ∀𝜎 ∈ M is admissible under Γ (2) ∃𝜎 ∈ M s. t. 𝜎[0] = init (3) ∀𝜎 ∈ M, ∀𝜎′: pre7ix of 𝜎, ∃𝜎′′ ∈ M, [. . . 𝜎′, dispose] is pre7ix of 𝜎′′ -
形式化(おまけ) 継続モナドとリアクティブプログラミング Formalization 今後の流れ ‧Routine の Monadic Chain を定義 ‧Routine
が Resource Safe → それらの合成も Safe を証明 ‧Source の理想的な定義が Safe であることを証明 ここでは説明しません 何故なら…… ☝出来ていない (修論にする予定) -
Row/Patch について ‧導⼊するか悩んでいるもの ‧連続値の現状の表現: リソースが消え、それと同時に別のリソースが現れた 継続モナドとリアクティブプログラミング Patch Semantics 0 1
2 0→1→2 と変化する時変値 = リソース 0, 1, 2 が順番に確保される Source -
Row型 そこで導⼊するのが Row 型 Row a: 変化する a 型の連続値 (Recap:
Source a: 変化するリソース集合) 詳しい data 定義は述べない (まだ決まっていない) 継続モナドとリアクティブプログラミング Patch Semantics -
Patch型 Patch a b: Row a に対するリスナー Routine ではダメなのか? ‧Routine
はそれぞれ独⽴したコンテキストを持つ ‧「前回の状態」を使いたい処理には向かない → 内部状態的なものを持つ必要がある 継続モナドとリアクティブプログラミング Patch Semantics -
Patch の性質 Patch a b は次のような定義を持ちそう Patch a b =
a Sync { result : Async b, next : Patch a b // 再帰的に呼べる } Patch a b は同じ対象に対する連続的変更を表す next を呼び、そこから得られた next を呼ぶ…を繰り返す 内部状態を持てる 継続モナドとリアクティブプログラミング Patch Semantics -
Patch は Monad ではない 継続モナドとリアクティブプログラミング Patch Semantics Patch a b
Monad → if による分岐が可能 Patch は内部状態を持つので 1. 毎回同じ Patch を実⾏する (if ⽂なし) 2. if ⽂は特別扱いし、グラフ再構築&内部状態リセット 筋がよさそう Patch b d Patch b c Patch c d if -
Arrow の登場 a から b への抽象化された⼿続き P a b が
Arrow → 変数の導⼊と静的な P a b に対する適⽤が可能 ArrowChoice → 途中で if 分岐が可能 ArrowApply → ⼿続き P a b が第⼀級になる Arrow → ArrowChoice → ArrowApply の順で強くなる 継続モナドとリアクティブプログラミング Patch Semantics -
Patch は ArrowChoice なしの Arrow ArrowChoice(分岐), ArrowApply(第⼀級関数) が無い これによって常に⼀本道、同じ順で評価される →
グラフの再構築もしない 継続モナドとリアクティブプログラミング Patch Semantics proc pattern -> do y <- f -< e z <- g -< e’ h -< e'' こんな感じで書けるが、分岐を書くと型エラー -
ArrowChoice なしのArrow ≒ Rules of Hooks ところで if が使えない →
Rules of Hooks に似ている ‧同じ順番で use… を呼ばなければいけない React は内部状態を Linked List で持っている 順番でのみ、Hooks の判別をする (グラフ構造が動的に変化してはいけない) → ArrowChoice なしの Arrow が適任 継続モナドとリアクティブプログラミング Patch Semantics -
発展: Agent ⽤ DSL ‧作りかけ ‧Agent 定義を簡単にできる ‧Durable Execution ‧Parallel
実⾏が楽 ‧Type Reflection、JSON Schema ⾃動⽣成 継続モナドとリアクティブプログラミング Component-based agent system Katari -
現在の Katari: 例 Discord bot の例 (DEMO?) 継続モナドとリアクティブプログラミング Component-based agent
system agent main(channel_id: string) -> string { use gemini.provider(model = "gemini-3.5-flash", api_key = env.get_secret(key = "GEMINI_API_KEY"), system =“他のユーザーからのメッセージに適切に返答してください。") use e2b.provider(api_key = env.get_secret(key = "E2B_API_KEY")) use tavily.provider(api_key = env.get_secret(key = "TAVILY_API_KEY")) use discord.provider(token = env.get_secret(key = "DISCORD_TOKEN")) let tools = [e2b.run_python, tavily.search] serve(channel_id = channel_id, tools = tools) } Function = Agent = Tool が成り⽴つ -
その他の機能 ‧MCP に接続、MCP を提供できる ‧Webhook に対する購読を管理できる ‧Coeffect (に似た仕組み)による Private 値トラッキング
‧Algebraic Effects による Context providing ‧ 継続モナドとリアクティブプログラミング Component-based agent system -
観察 ‧MCP に接続、MCP を提供できる ‧Webhook に対する購読を管理できる serve(channel_id = channel_id, tools
= tools) : キャンセルするまで動き続ける、discord 監視 & 応答 初期化 & 解放 → リソース 継続モナドとリアクティブプログラミング Component-based agent system -
現状の問題点 ‧状態の動的な更新が難しい ‧例えば、Claude Code における "Mode" ‧購読の⼀時停⽌、再開 ‧動的なプログラム更新が難しい ‧たとえば ‧Scrap
& Build するしかない → 動的な状態 継続モナドとリアクティブプログラミング Component-based agent system -
将来計画 リソース、動的な状態 → リソース指向リアクティブ‧プログラミングの出番 リアクティブプログラミングを応⽤して Agent Network を構成できそう ‧サンドボックス ‧分散システム(→Server
Components?) ‧権限周り、コンテキスト → ここら辺をコンポーネントシステムで統括する 継続モナドとリアクティブプログラミング Component-based agent system -
まとめ ‧リソース管理は⼤事 ‧リアクティブプログラミングは便利 ‧合体→なぜか継続モナドが出てくる ‧継続モナドのネスト構造がコンポーネントのネストに対応 ‧Patch は ArrowChoice なしの Arrow
‧React の Rules of Hooks の制約 ‧ Agent Network 構築に使えそう 継続モナドとリアクティブプログラミング