webfuiが面白い
Land of Lispの作者が作っているClojureScriptのフレームワーク、WebFUIが面白い。僕がJS書くときによく使うPastaとよく似ているので盗めるところは盗みたい。
状態
イミュータブルな辞書で状態を表現して、リファレンスを張り替えることで状態の遷移を行うところはPastaと共通。この方法はよくあるオブジェクト指向のMVCフレームワークで行われるミュータブルな辞書をその場で書き換える方法と比べて、
- 後で必要になるデータをcloneなしで取っておける.(もちろん必要ない物はGCされる)
- 同じ状態の時に必ず同じUIになることを保証できる.
- (上記に関連して、)アプリの状態をそのままサーバなりローカルストレージなりに突っ込んで、そこからUIを含め完全に状態を復元できる
などの利点がある。
状態の辞書を作るのにパッチ(状態の部分的な辞書)を返すというのもPastaと共通。assocとかconjとかをユーザ(プログラマ)にやらせるより安全だしユーザも楽だしそうなりますよね。
状態 -> UI
作った状態をUIに反映させる部分がPastaとWebFUIの最大の違い。Pastaではパッチを舐めてViewと呼んでいるモジュールの、パッチのフィールドと同名の関数に現在の状態と以前のそのフィールドの値を渡し、ユーザはその関数内でjQueryなりなんなりを使ってDOMを直接いじれることとして、Pastaはそれに関知しないというスタンスを取っている。
一方WebFUIでは、Clojureでのhtml生成によく使われるHiccupと同じ方法(と同じ構文)で、Webページ全体のHTMLを表現する純粋データ(DOM EDNと呼ばれる)を作る関数をユーザに書かせて、状態が差し変わる度にこれを呼んで、新しいDOM EDNを作り、古いDOM EDNと比較して変更のあった部分を実際のDOMに反映する。
Pastaではモジュールを作る必要があった部分をWebFUIでは一つの関数に圧縮できるのでWebFUIの方が1段抽象度が高い。ただし、毎回巨大なDOM EDNを生成するコストと、デルタ(差分)を計算する(DOM EDN総嘗め)コストはかなりかかるし、ClojureScriptそのもののオーバーヘッドもあるので速くはない。僕は速さか宣言性なら宣言性を取りたい。
コントローラ
DOMイベントを拾って状態を変更する関数(PastaではModel, WebFUIではwatcherと呼ばれる)に渡す部分は似たり寄ったり。Pastaはブラウザへの依存をなくすためにpasta_signalと呼ぶ関数を提供してそれをイベントリスナに仕込むことでModelを呼ぶ。WebFUIではDOM EDNにあらかじめウォッチする要素を仕込んでおき、add-xxx-watch関数でwatcherを定義する。watcherが受け取る状態以外の引数はwatcherの種類によって違うものになる。add-watchという名前は微妙によくない。defwatcherとかのほうがLispらしいと思う。トップレベルにdefxxx以外の動詞があるのは若干気持ち悪い。
通信
WebFUIでのサーバとの通信とかはどうやるのかはまだ見てなくてよくわからない。PastaではModelの関数中で状態のurlフィールドとかを変えてやってViewでajaxしてコールバックにpasta_signalを仕込むか、状態の辞書にajaxのプロミスを突っ込む。
コード
百聞は一見に如かず。コードで比較してみる。これはWebFUIを持ち上げる記事なので、WebFUIのお家芸のリアクティブプログラミングっぽい足し算の例にする。コードは動かしていない。
//Pasta var Model = (function () { return _.module({}, inputValChange, reset); function inputValChange (state, data) { return _.assoc({}, data.field, data.val, "sum", state.a + state.b); } function reset (state) { return { a: 0, b: 0, sum: 0 }; } }()); var View = (function () { return _.module({}, sum); function sum (__, state) { $(".expr").text(_.simplate("{{a}} + {{b}} = {{sum}}", state)); } }()); (function () { var pasta_signal = Pasta(Model, {}, View, {a: 0, b: 0, sum: 0}); $(".addition-in").keyup(pasta_signal("inputValChange", function (e) { return { field: $(e.target).attr("data-field"), val: parseInt(e.target.value) }; })); $(".addition-reset").click(pasta_signal("reset")); }());
;; WebFUI (defn render-all [{:keys [a b]}] [:div [:input {:data-field :a :value a :watch :addition-watch}] " plus " [:input {:data-field :b :value b :watch :addition-watch}] [:p a " + " b " = " (+ a b)] [:button.reset {:mouse :mouse-reset} "reset"]]) (add-dom-watch :addition-watch [state new-el] (let [{:keys [data-field value]} (second new-el)] {data-field (js/parseInt value)})) (add-mouse-watch :mouse-reset [state first-el last-el] {:a 0 :b 0}) (launch-app (atom {:a 0 :b 0}) render-all)
適当まとめ
読みやすさは、S式だからというのもあるけど、それを差っ引いてもWebFUIの方が数段上だと思う。ページ全体のDOMをクライアントサイドで生成することが許容され、スピードもある程度妥協できる場面では積極的に使ってみたい。