標高+1m

Don't be rational

webfuiが面白い

Land of Lispの作者が作っているClojureScriptのフレームワーク、WebFUIが面白い。僕がJS書くときによく使うPastaとよく似ているので盗めるところは盗みたい。

Clojure/conjでの動画
githubリポジトリ

状態

イミュータブルな辞書で状態を表現して、リファレンスを張り替えることで状態の遷移を行うところはPastaと共通。この方法はよくあるオブジェクト指向MVCフレームワークで行われるミュータブルな辞書をその場で書き換える方法と比べて、

  1. 後で必要になるデータをcloneなしで取っておける.(もちろん必要ない物はGCされる)
  2. 同じ状態の時に必ず同じUIになることを保証できる.
  3. (上記に関連して、)アプリの状態をそのままサーバなりローカルストレージなりに突っ込んで、そこから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をクライアントサイドで生成することが許容され、スピードもある程度妥協できる場面では積極的に使ってみたい。