二種類のリアクティブな値の動的切り替えとモナドの関係
前提知識
コードは PureScript および Haskell っぽく書いていますが、ReactiveX の対応する概念を書いてあるので Rx を知っていればなんとなく読めると思います。モナドはそういうのがあるんだな~と読み飛ばしておけば OK です。
結論
プロダクトにおいて、例えば React ならコンポーネントを動的に差し替えたりしたり、例えば Unity (with UniRx) なら動的にオブジェクトを作成したりするといった、動的にリアクティブな値を切り替えることがある
リアクティブな値の動的切り替えは次の2つの方法で構成できる
- Rx の Switch と同様
- mFRP
どちらも動的な切り替えを可能にするが、前者は外側からのライフサイクル管理、後者は内側からのライフサイクル管理ととれる。
どちらも、次の関数 join
を作ることで成り立っているが
Switch
join :: forall a. Signal (Signal a) -> Signal a
mFRP
join :: forall a b. Signal a (Signal a b) -> Signal a b
これはモナドを構成する。
はじめに
ここでは Reactive Programming におけるリアクティブな値(状態的な何か)を扱って、Event は扱わないものとします。
a
型の Reactive な値を Signal a
とします
Signal a
は値の変更を伝播することができ、例えば ReactiveX
なら Select
および CombineLatest
の実装、Functional Reactive Programming なら map
および liftA2
の実装によって達成できます。
map_select :: forall a b. (a -> b) -> Signal a -> Signal b
liftA2_combineLatest :: forall a b c. (a -> b -> c) -> Signal a -> Signal b -> Signal c
これらの関数によって状態の変更を伝播することができます。例えば関数 f
と Signal Int
型の値 x
, y
があったとき
x :: Signal Int
y :: Signal Int
f x y = x + y
次のようにすると x
または y
が変更されたときに同期されるように変更される Signal z
が構成できます。
z = liftA2_combineLatest f x y
リアクティブな値の動的な切り替え
ところで、単純な FRP ではアプリケーション初期化時にすべてのリアクティビティ(Signal)のつながりが決定されています。
これは map
および liftA2
のみで組み立てているからです。基本的に Applicative は逐次的な実行を行う機能を提供しません(すごくあいまいな話ですが……)
しかし実際のプロダクトでは、それぞれの要素がライフサイクルを持っていて、例えば Web アプリだったら動的にページを切り替えたり、タブを切り替えたりしますし、ゲームだったら動的にオブジェクトを生成します。
そこで、Signal
のつながりを動的に切り替えたいという要望が発生します。
ここでは二つの方針で Signal の動的な切り替えを表現したいと思います。
Switch と同様なもの
Reactive X には Switch
という Operator があります。これは次のようなことができます。
switch :: Signal (Signal a) -> Signal a
これは Signal a
のサブスクライブを外部の Signal に合わせて次々と切り替えていくような動作をします。
ReactiveX のサイトの図がわかりやすいです。
注意点として、同じく ReactiveX にある FlatMap
は少し違う動作をします
これは、切り替え時に前に Subscribe した Signal を Unsubscribe しないので、Signal の切り替えという点からは不自然です。
この Switch
はまんまモナドの join
と同じですが、予想通りモナドになります。モナド則はいい感じに絵を描けば成り立つのがわかるので、ここでは省略します。
また、この join
から作った liftM2
は先ほどの liftA2
と同様になります。
mFRP
mFRP は次の記事で分かりやすく紹介されています。
mFRP における Signal
は先ほどの定義と少し違っていて、型引数が一つ多いです。
Signal a b
この Signal
は基本的に a
型の値を発行しますが、最後に b
を発行します。
ReactiveX でいうところの、 OnComplete の時のみ OnNext とは違う型を発行する感じです。
mFRP ではこの Signal におけるモナドを提供しています。
join :: forall a b. Signal a (Signal a b) -> Signal a b
これは Rx でいえば OnComplete 時に別の Signal を発行するような Signal をとり、それらを平らに接続するものです。
比較
Switch タイプでは、外側の Signal
に合わせて内側の Signal
を切り替えるような動作をします。
対して mFRP では外側の Signal
の次に内側の Signal
を実行するような動作をします。
ここで注目したいのは、mFRP では Signal
自体が完了アクション、OnComplete を持っていることです。
mFRP では Signal
を構築するときに終了アクションを組み込み、外側から二つの Signal
を連結する、といったことをしている一方、Switch タイプでは内側の Signal
は自身のライフサイクルに関する知識は持たず、外側の Signal
によって強制的に切り替えられます。
これが結論で述べた Switch は外側からのライフサイクル管理、mFRP は内側からのライフサイクル管理ととれる理由です。
諸概念との対応
ほかのプログラミングにおける諸概念との対応を考えてみます。
React の useState と Hooks
React ではコンポーネントという単位でアプリケーションを構築します。観察すると、コンポーネント自体は自身のライフサイクルを管理せず、親が子コンポーネントのライフサイクルを管理しています。したがって Switch タイプのモナドと対応していると考えられます。
具体的に、コンポーネントは、VDOM が次々に変わる何か、という感じで見れます
component1 :: Signal VDOM
component2 :: Signal VDOM
...
これらのコンポーネントを動的に切り替えるような Signal を考えてみます。
componentSwitched :: Signal (Signal VDOM)
これを join
ないし Switch
で平らにすることで親コンポーネントが作成できます。
parentComponent :: Signal VDOM
parentComponent = join componentSwitched
もう少し広く Web アプリの状態ととらえる
先ほどは React と述べましたが基本的に HTML の構造がこれらと相性が良いです
例えばページ切り替えがあったとします
ページ本体は適当に HTML
型として、何ページか用意してみましょう
page1 :: Signal HTML
page2 :: Signal HTML
page3 :: Signal HTML
ページの選択状態を表す Selected
型を用意し、その実際の値を表す selected
があったとします
data Selected = One | Two | Three
selected :: Signal Selected
selected = ...
selected
に併せてページを切り替えるような Signal
を作ります
pageSwitched :: Signal (Signal HTML)
pageSwitched = map_select selectPage selected
where
selectPage One = page1
selectPage Two = page2
selectPage Three = page3
これを join
ないし Switch
で平らにすることでサイト全体を表現した Signal
が作れます
site :: Signal HTML
site = join pageSwitched
UniRx 使用時の MonoBehaviour のライフサイクル
Unity (with UniRx) では様々なコンポーネントを動的に作成します。例えば HogeComponent
を考えます。HogeComponent
内にはいくつかの ReactiveProperty があるでしょう。
その中から特に Int 型の ReactiveProperty を選択するような関数を用意します。
extractIntRP :: HogeComponent -> ReactiveProperty Int
HogeComponent
を管理する親コンポーネントがあった場合、その中には次のような ReactiveProperty があるでしょう。
currentHogeComponent :: ReactiveProperty HogeComponent
すると次のようにすることで、切り替わっていく HogeComponent
の中の、Int 型の ReactiveProperty を入手できます
intRP :: ReactiveProperty Int
intRP = switch (map_select extractIntRP currentHogeComponent)
LINQ で書くならこうです
var intRP = currentHogeComponent.Select(extractIntRP).Switch();
また、mFRP の観点から見ると、自身のライフサイクルを自分で管理する場合、すなわち OnDestroy()
を自分自身で呼ぶ場合は mFRP っぽくあるのですが、ReactiveX は OnComplete 時に別の値を発行することはないので、mFRP のようなオペレーターはありません (たぶん)
ちなみに
自身で作ったライブラリ purescript-jelly
は Switch 方式のモナドを搭載しています。
特に Web アプリなどでは親から子のライフサイクルを管理することがほとんどですので、Switch 方式のほうが自然に状態管理ができます。
まあ時と場合って感じですね……
参考
モナディック関数型リアクティブプログラミング(mFRP)の実装のひとつmoffyの紹介 https://zenn.dev/yoshikuni_jujo/articles/introduction-to-moffy
ReactiveX https://reactivex.io/
traP (デジタル創作同好会 traP)は、 東京科学大学で最も活発なデジタル創作・プログラミング系サークルです。Zenn 上では主にメンバーによる技術ブログを掲載していますが、普段はホームページ上で投稿しています。こちらもぜひご覧ください。trap.jp
Discussion