Chapter 04無料公開

Bow Arch (Comonadic UI)

Yasuhiro Inami
Yasuhiro Inami
2020.09.23に更新
このチャプターの目次

それでは最後に Bow-Arch についてお話しします。


Bow-Arch は、スペイン在住の Tomás Ruiz-López さんが開発したUIアーキテクチャになります。

Bow-Arch について簡単に説明しますと、まず Bow というライブラリがあります。
いわゆる高カインド多相というテクニックを使った関数型プログラミング用のライブラリで、
簡単に言うなら、protocol Monad みたいなコードを Swift 上で書くことができます。
これを使うと、通常のSwiftでは考えられなかったUI設計ができるようになり、
Bow-Arch はいわゆる Comonadic UI という 圏論のテクニックを使ったUI設計手法 を提供します。


Comonadic UI

Comonadic UI。
実はこの話が、Optics という前菜の後の本日のメインディッシュになります。
初見ですと難易度がかなり高いと思いますので、
昼間からお酒でも飲みながらカジュアルに聞いてもらえれば幸いです。


まずはコモナドとは一体何なのか。
一言で言ってしまえば、 オブジェクト指向 です。
もう少し正確に言うと、オブジェクト指向から 可変参照を除いた、よりピュアな構造 、といえます。
大まかには、 内部状態を持ち、状態から計算した結果を返す ことができます。
例えば、イテレータパターンですとか、ビルダーパターン
さらには React Component などがまさにその性質を満たしていると言えます。


通常、私たちが SwiftUI でビューを作ると、ざっくりこのような形になりますが、
実はこれを コモナドをみなすことができます
ここで body という関数に注目してください。
実はこの型 (Self -> V == Component<S, V> -> V == W<V> -> V) を眺めると、
コモナドを語る上で重要な性質の1つである extract という関数が見え隠れします。


extract ならびにコモナドの説明として、
やはりここでも圏論の図を使ってお話ししてみたいと思います。
こちらは、2年前のiOSDCで使った随伴のスライドになりますが、簡単に言うなら、
「随伴」というのは、2つの圏があり、ピンク色の矢印の集合に1:1の関係があり、
2つの圏の間に「左随伴関手 F」 と 「右随伴関手 U」 がある状態のことを指します。


この F と U を交互に適用すると、このような三角可換図式をそれぞれの圏で書くことができます。


で、随伴を考えると何が嬉しいかというと、実はそこから モナド を作り出すことができます。
具体的には、右の圏から左随伴 F、右随伴 U、F、U、と二往復します。
すると、あっという間に右の圏でモナド (UF) が生まれます。
モナドの特徴は、returnjoin という2つの赤い矢印の関数を持つことです。


では、それを逆に考えたらどうなるか、というと、実は コモナド が作れます。
具体的には、今度は左側の圏からU、F、U、Fと二往復します。
すると、今度は左側の圏で赤い矢印2本が、今度は外に広がる形で現れます。
そのうちの一つが extract 関数で、もう1つが duplicate という関数になります。


このモナドとコモナドの話を、擬似Swiftを使ったコードで表すとざっくりこうなります。
Monadは returnjoin の2つ。
Comonad は extractduplicate の2つ。

ちなみに、今回は分かりやすさのために疑似Swiftを引き続き使いますが、
同様の実装を Bow ライブラリを使って置き換えることも可能です。


さて、コモナドの特徴となる extractduplicate ですが、
extract は先ほどSwiftUIで説明した body に相当します。
つまり、状態からViewを計算して抜き取ります

一方で、duplicate はどのような関数なのでしょうか?
実は、コモナドを理解する上で重要な鍵を握るのがこの duplicate なのですが、
一言で言ってしまえば、これは コモナド自身の未来を写し出す関数 だと言えます。
・・・未来を写し出す、というと、なんだかオカルトっぽい響きですよね。
でも本当にそのような不思議な性質を持ち合わせているのです。


例えば、自然数の無限ストリームのコモナドを例に取ってみます。

これを duplicate をすると、文字通り、元のストリームを複製 しますが、
ただ複製するだけでなく、複製して1回シフトする、また複製して2回シフトする、
といったことを延々と繰り返して、 無限ストリームの無限ストリーム が出来上がります。
なんとも不思議な性質ですが、これは無限ストリームのデータ構造からこのように実装できます。
ひとまず、duplicate とはこのように、未来の自分の全パターンの集まり だと思ってください。


さて、今度は無限ストリームではなく、 UI Component の場合はどうなるでしょうか?
実はこの場合も、似たような結果になります。
UI Component を duplicate すると、現在のComponentをただ複製したもの
複製してから一部の状態を変更したもの
さらには 別の状態に変更したもの・・・の全てが詰まった、
いわゆる Componentが取りうる状態空間をなす 、と考えることができます。
これがコモナドが満たすべき duplicate という、もう一つの特徴です。


さて、ここまで Component という型の性質をざっくり見てきたわけですが、
実はこの構造は、関数型プログラミングの世界では、
Storeコモナド と呼ばれる構造として知られています。
つまり、 内部状態と render 関数のペア と思ってもらえれば良いです。
ここで render 関数は、内部状態からビューを生成するものですが、
ただそれだけだと、役割としては単なる「JSON色付け係」に過ぎません。
実際のビューはもう一つの役割、すなわち 「外部からユーザー操作を受け付ける」 という
重要な機能がありますので、もう少し実装を拡張してみましょう。


するとこのように、イベントハンドラーも合わせて必要ということになります。
(NOTE: IO<Void> は副作用の型。Swiftには必要ないですが、あえて明示的に書くことにします)

ただ、この場合も、Component はコモナドの構造を維持します。


それは、EventHandler を含んだ typealias UI を定義することによって、
Component<S, V>Store<S, V> の代わりに Store<S, UI<V>> として表せる からです。

コモナドの説明に関しては、以上になります。
・・・ここまで大丈夫そうですかね?


それでは次に、コモナドの内部状態を外から操作する方法 について見ていきます。
例えば、先ほどの無限ストリーム (Stream コモナド) の場合、
shift操作が状態変更とみなせる わけですが、実は先に答えを言ってしまうと、
shift操作は 「モナドを使ったクエリDSL」 で表すことができます。

ここで疑問に上がるのが、

  1. Shift モナドはどこから出てきたのか?
  2. Stream コモナドに対する Shift モナドはどのような関係にあるのか?

という2点です。


まず後者の質問からお答えすると、
Stream コモナドと Shift モナドは、Pairing という関係を持ちます
Pairing の解説は少し難しいのですが、簡単に言うと、
アプリティブの合成のような性質を持っていて、
コモナドとモナドという異なる型同士を合体して計算できる
というものです。

実装例としては、先ほどの StreamShift が スライド下部のコードのように
static func pair を実装することで、 Pairing に準拠できます。

例えば、もし Stream が前述の [0, 1, 2,...] 無限ストリームで、
Shift モナドの値が「2回shiftして完了」というクエリだった場合、つまり、

// [0, 1, 2, ...]
let stream: Stream = .cons(0, .cons(1, .cons(2, ... /* 以下、無限に続く */ ...)))

// 2回 shift するクエリ
let shift: Shift = .shift(.shift(.done))

だった場合、static func pair では、

  1. パターンマッチで .shift になるので、後者の case に行き、stream の先頭の値 0 を捨てて、後続の nextStream = [1, 2, ...] を抜き出し、再帰する
  2. パターンマッチで 再び .shift になり、stream の先頭の値 1 を捨てて、後続の nextStream = [2, 3, ...] を抜き出し、再帰する
  3. パターンマッチで .done になり、前者の case に辿り着き、先頭の値の 2 を使った何らかの計算 f が行われる(nextStream = [3, 4, ...] は捨てられる)

確かにクエリ通り、 無限ストリームを2回shift処理している ことが分かります。


では、この Pairing を使うと何が嬉しいのでしょうか?
それは、コモナドが duplicate によって未来の自分を見渡すことによって、
モナドの操作クエリと合体させて、未来のうちの1つを選ぶことができます

言い換えれば、モナドによってコモナドの内部状態を変更して、新しいコモナドを手に入れる ことができます。
それを select という関数で表すと、このようなコードが書けます。
StreamShift のペアだけでなく、 任意の Pairing に対して select 関数が使えます


他にも例として、SwiftUI のビューの場合は ComponentStore コモナド)でしたが、
これに対応する操作用モナドを考えることができます。
そして実は、そのモナドとは 状態モナド のことを指していて、
状態モナドから次の Component コモナドを計算する ことができます。


さて、ここまで天下り式に、

  • Streamコモナドのペアとして Shift モナド
  • Store コモナドのペアとして State モナドが得られる

という話をしてきましたが、
そもそもこの コモナドに対するモナドのペアはどうやって導くのか
というのが最初の疑問でした。


実は、その答えとしては、このスライドにある struct Co という型を使うことで、
コモナドから自動的にペアとなるモナドを導くことが可能 です。
この型は、圏論でいう (Comonad W に沿った恒等関手への) 右 Kan Lift というものを使っていて、いわゆる 右 Kan 拡張の双対バージョン になります。

細かい話は今回置いといて、今はありがたくこの型を使うこととしましょう。


すると、例えば Store コモナドに Co を使うと、State モナドを自動的に導くことができます。
ここでの型計算による証明では、カリー化と米田の補題を使っています。


ここまでの話をまとめてみましょう。

  • まず、コモナドには extractduplicate の2つの関数がある
  • extract は、状態から 仮想View を生成し、 duplicate はUIの未来図を作る
  • struct Co を使うと、オブジェクトのコモナドに対する状態操作のモナドを作ることができる
  • そのモナド・コモナドのペアに select を適用して、未来のコモナドの1つを選択する

という一連の流れです。

・・・ここまでOKですかね?


さて、ここまでコモナドは「オブジェクト指向プログラミングのようなもの」、
とお話ししてきましたが、実際のオブジェクト指向では、
可変参照とその変更を伴う副作用 がカジュアルに発生しますので、
ここで副作用に対応する コモナドのラッパー型 を用意します。
これを Effect Component と呼び、SwiftUI対応をすると、およそこのようなコードが書けます。


EffectComponent について簡単にまとめるとこの通りです。
要するに、コモナドに副作用を掛け合わせた EffectComponent は、
オブジェクト指向プログラミングそのもの ということです。

そして、コモナド部分に Store を使った場合、状態操作は State モナドになりますが、
これに 副作用が伴うと、実は React の世界における setState に相当します
あるいは、SwiftUI でいう @State の可変参照のセッター と同じです。
つまり、状態を直接的に更新している わけですね。

これをもって、Storeコモナド」と「React」「SwiftUI」の世界は共通している
と言うことができます。


ところが、この話が本当に面白くなるのは、実はここからなんです!
まだ後2回の変身を残しています。

残念ながら、今日は時間の都合上、触りの部分だけお話ししますが、
実はこの EffectComponentStore コモナド以外にも使うことができるのです!


その代表的な一例が、Moore コモナド です。
Swiftで書くとこのようなコードになるのですが、実はこれ、ムーア状態機械と呼ばれるもので、
「初期状態」、「Reducer」、そして「出力関数」の3つ組と同型 になります。

この型は、実質 Reducer を持っていると考えられるので、
まさに Redux や Elm Architecture そのもの ですね。

また、アクションを介して 状態が間接的に更新 されます。ここが React との大きな違いです。
しかし、同じ EffectComponent を使っていることには変わらず、
そのアーキテクチャの違いはコモナドによって決まります。


そして、さらにこの話を抽象化すると、どうなるか。

実は、余自由コモナド(Cofree コモナド) という、
任意の関手からコモナドを作る型 を使うことができます。
例えば、先ほどの Store コモナドや Moore コモナドも
この Cofree コモナドによって表すことが可能です。

Cofree コモナドのペアに相当するものが、
いわゆる 自由モナド(Free モナド) と呼ばれるものになります。

この Cofree を使ったUI設計に近い例としては、
PureScript というWebの言語で実装されている Halogen が挙げられます。
このフレームワークでは、Cofree こそ使われていませんが、
ペアの部分で Free モナドが使用されています。
(NOTE: 実際のアーキテクチャーはもっと複雑です)


というわけで、今の話をざっくりまとめると、
コモナドに対応するUIアーキテクチャー がこのように対応付けられます。

すなわち、SwiftUI や React、Elm、さらには PureScript Halogen といった
個々のフレームワークが、コモナド1つを差し替えるだけで説明できてしまう。

これが Comonadic UI の概要になります。

もしアプリのUIアーキテクチャーに興味がある方は、ぜひこの分野に目を通してみると良いでしょう。


ここまでの話を一句にまとめるなら、
「コモナドは アーキテクチャーを 規定する」 になります。


Comonadic UI の理論的な背景の解説は、こちらの論文をご参照下さい。

読み終えた頃には、「気分はコモナディック!」になっていることでしょう。