Fluxに関する独自解釈と妄想を、何かの翻訳っぽく書いた。
フレームワークを作る時、我々は「簡単に記述する」ことを第一に考えがちだ。
そして、簡単にするための仕組みはウケる。
逆に記述量が増えるとウケない。
しかし例外があって、多く書くことによるメリットが受け入れられたときは別だ。
例えば、Backbone.jsを使うと記述量が増える事は誰もが認めるところだが、MVCの実現というメリットのために広く受け入れられた。
要するにトレードオフなのだ。
ここのところFluxアーキテクチャが注目を浴びているが、書いてみると記述量は相当増える。
そもそも登場人物が多すぎる。
Action、Dispatcher、Store、View、それからそれらの間に挟まって仕事をする者達。
一体彼らは何をしたいのか。
最近になって分かってきた。
これはアプリケーションそのものを抽象化したアーキテクチャなのだと。
Fluxは何がしたいのか
FluxはMVCを置き換えるものとして登場した。
本当にそうだろうか。
Actionは必要か
例えば、AngularJSやBackbone.jsはActionを必要とせず、そのままモデルを更新する。
あるいはビューが直接モデルを更新するのは責務上よろしくないとか、ロジックの共通化という目的で、例えばコントローラに関数を置いたりする。
しかしActionは登場しない。
むしろ直接関数を呼び出す方が手軽なのでは?
結論から言うとActionは必要だ。
全ての出来事はデータだ
データベースを普通に使うと大抵は最後に反映された状態のみを保存しているため、それがどういう経緯で作られた状態なのか復元できない。
しかし、全てのイベントを保存しておけば、過去から現在の全ての状態が再現できるという考え方がある。
そうは言っても無限のリソースを持っているわけではないので、どこかで情報を圧縮することにはなるのだが、確実に言えることはイベントの集積が情報として一番強いということだ。
そして関数を適用するにしたがって、計算結果を得る代わりに次第に情報量は減っていく。
こうしておけばDBに専用のフラッシュバック機能は必要ない。
出来事はそれがどのような用途に使われるかを知らない
「いつ誰がどこで何をした」という出来事は、特定の用途には情報過多かもしれない。
しかし安易にそれを捨ててはいけない。
後からアドホックに追加された機能によって、別の可能性を見出されるからだ。
ある機能は「何をしたか」によって処理を切り替えるかもしれない。
しかし後から追加された機能が「どこでそれが行われたか」でフィルタリングして分析を開始するかもしれない。
順番に届いた出来事を好きな人が好きなだけ持っていく
Actionを実際にDBに保存するかはさておき、何らかの手段でそれらはキューの形で運ばれてくる。
同じ情報を使う機能がいくつもあるということになれば、Pub/Subモデルが適している。
そういうわけでDispatcherが登場する。
DispatcherはただPub/Subの仕組みを提供するだけで、具体的にどんなSubscriberがいるかには無関心だ。
スケーラビリティは不可逆
このように順序立てて考えていくと、Fluxが見えてくる。
ここで重要なのは、このアーキテクチャは最も理想的なアプリケーションの構造を抽象化したものだということだ。
実際に必要か否かに関わらずActionは存在するものだ。だからそれを表現した。
そうなると書きやすさは二の次だ。
おそらく目の前の書きやすさにフォーカスするのなら、必要なのはFluxではなくAngularJSだ。
Fluxはもっと大規模を想定する。
最近よく考えるのは「スケーラビリティは不可逆」だということだ。
サーバー台数を横に増やす話ではなく、ここで言っているスケーラビリティとはなるべく同じコードを保ったままアプリケーションの構成を変えられるという意味だ。
マイクロサービスが話題になっているが、何も考えずにモノリシックに書き始めるとマイクロサービスに拡張することは永久に不可能だ。
そこで、じゃあ最初から考えろよという話に普通はなるのだが、ここで追求する理想は、考えなくても拡張できる状態になっていることだ。
言い換えれば、小さいインフラでも大きなサービスと同じ書き方をしておいてスケーラビリティを確保せよという話だ。
小さいうちから大げさに?いや、大げさであると思わせないほど簡潔に表現するのだ。
そのためには、何かしらの言語なりフレームワークが必要だ。
Fluxが出てきた時に確かに「MVCはスケールしない」と言っていたのだが、MVCのスパゲッティ状態を解消する目的という話に発散してしまったようだ。
それで、なるほどAltMVCかと思って考え始めたのだが、どう考えてもActionが必要なかった。
しかし、スケーラビリティに注目すると色々と辻褄が合う。
そうなると全然フロントエンドだけの話ではない。
全てを支配できる。
以降、追加でスケーラビリティ実現に何が必要かを考えてみる。
本質的でない状態を排除する
主にサーバサイドにおいてスケーラブルなアーキテクチャを指向してアプリケーションを書き始めると、ひとつの気付きがある。
状態を管理する必要が全くないという事だ。
昔からステートレスにしなさいとは言われていたが、Amazonに至っては思い切ってLambdaと表現するなどしている。
言ってしまえば、アプリケーションとはアクションと古い状態を入力として新しい状態を返す関数ということになる。
型をつけるとこうなる。ちなみにモナドではない。
application :: Action -> State -> State
ここでいう状態というのは例えばDBなどの事を指していて、決して計算途中の値のことではない。
forループの外にあるsum
変数などは本質的な状態ではない。あるいは設計の都合上たらいまわしにして構築されるオブジェクト、これも状態を持つ必要はない。
言い換えれば全て純粋な関数で書けるということだ。
しかし我々は「慣れていて書きやすい」という理由で不用意に状態を扱ってしまう。
例えば先ほどの関数で、新しい状態を返す代わりに古い状態を書き換えたらどうなるだろうか。
関数の呼び出し側は新旧の値の比較が出来なくなってしまう。
実際、このことがReact.jsの最適化を妨げる要因となっていて、Immutableを売りにした類似フレームワークは軒並みパフォーマンスが高い。
他にも有名な例としては、リストから新しいリストを作るときにmap関数を使うかforループを使うか、というものがある。
val newList = oldList.map(_ + 1)
簡潔に書けているが、問題はそこではない。
重要なのは、既に並列計算のための準備が出来ているということだ。
val newList = oldList.par.map(_ + 1)
for文でこうはいかない。
手続き型言語では、知らず知らずのうちにスケーラビリティを落としているケースがあるのだ。
何故か。
状態を変更する方法だけを提供すべき関数がそれを実際に適用してしまったり、各リスト要素の変換方法だけを提供すべき関数がリストの作り方にまで言及しているからだ。
こういう事が平気で起きてしまうのは、純粋関数型言語以外は副作用の有無を区別しないからだ。
しかもだんだん規模が大きくなってくると、どこでそういうことが行われているかが全く分からなくなる。
そしていざという時になってHadoopへの移行は無理だね、という話になる。
デフォルト非同期
また少し違う観点で話をすると、Node.jsのような非同期ベースは最早当たり前にあって良い。
Node.jsで現状不満なのは、非同期のほうがコード量が増えるということと、非同期APIと同期APIが全く別の書き方を要求するということだ。
しかしこれはシンタックスの問題なので、非同期処理が簡単に書ける言語があれば何の問題も無い。
非同期処理を同期処理の切り替えが自由になるのは都合が良い。
例えば、JavaScriptではlocalStorageが同期APIなのだが、抽象化のために非同期APIでラップするとIndexed DBとの乗換えが楽になる。
もっと言うと、RPCを使ったコードを綺麗に書ける可能性を秘めている。
先ほどの.par
のように簡単に切り替えられるとか。
最強の抽象化で勝負に出る
総合すると、スケーラブルな言語やフレームワークの要求仕様とは次のようなものだ。
- Actionをデータとして扱う ⇒ 通信手段、再現性に対して柔軟
- Dispatcherを使ったPub/Sub ⇒ 機能拡張に対して柔軟
- Immutableなデータと純粋な関数を使う ⇒ 並行性、物理構成に対して柔軟
- デフォルト非同期 ⇒ 同期処理と非同期処理の切り替え、通信手段に対して柔軟
最初から「疎結合」と言えばそれで済んだのかもしれないが、それではコードレベルに落ちないのでこれで良い。
あとはこれを超書きやすくするだけだ。
書きやすくなければ意味が無い。
特にImmutabilityや非同期処理の書きやすさは言語レベルのサポートがないと無理だ。
頑張れば出来るかもしれないが、やりたくない。
そろそろJavaScriptを捨てる時が来ているのかもしれない。
余談だが、Immutabilityはフロントエンドからサーバ、クラウド、インフラ、DB、どこへ持っていっても良いものだという感触がある。
色んな意味でリソースが贅沢に使えるようになったおかげだろう。