Pasta(deprecated)とかkakahiakaとかcastorocaudaっていうのを2012年くらいからぽつぽつ作ってて、その時に考えていたこと。多分Fluxも同じことを考えてて思いついたんだと思ってる。あと継続ベースのWAF( Seasideとかkahuaとか )とも通じる部分がある。*1
狭義の関数型プログラミングには値と関数しかない*2ので状態が持てない。つまり入力から一意に出力が決まるプログラムしか書けない。つまりアプリケーション自体が一つの関数である必要が有る。
こういうアプリケーションの典型例としては、対話式でない単純なシェルスクリプトがある。完全にステートレスな(裏にDBがあったりしない)Webサーバの各エンドポイントとかも当てはまる。
$ echo hello hello
このパラダイムで対話的UI(例えばWebフロントエンド)を作るには、シェルの役割を果たすフレームワークがあれば、プログラマは純粋関数を書くだけで良い。つまりフレームワークの責務は、
- ユーザの入力(マウスクリック、キープレスなど)の度に適切な関数を適切な引数で呼ぶことと、
- この関数の返り値を利用してUIをアップデートすること。
実はこれだけでは不十分で、フロントエンドアプリケーションは状態を持つ必要が有ることが多い。ここでいう状態は、アプリケーションのライフサイクルを通じて、刻々と変わっていく変数の集合。例えば記事一覧と記事詳細を扱うアプリケーションなら、記事データの配列と、現在選択中の記事IDなどが該当する。
そこで先の架空のフレームワークに一つインディレクションを加えて、
- ユーザの入力がある度に適切な関数を適切な引数で呼ぶ
- この関数は現在の状態と1から渡ってきた引数を受け取り、新しい状態を返す
- 新しい状態から新しいUIを組み立てて表示する
こうする。
アプリケーション全体を見ると、状態からUIへの純粋関数を、繰り返し状態を変えながら呼び出している構図が出来上がる。
理論的にはこれでいいんだけど、3は安直にやるとページ全体が書きかわることになってSPAの意味がなくなる。そこでWebFUIやcastorocaudaやReactでは、プログラマには状態から仮想DOMっていうツリー状のデータを生成する関数を書かせて、これを前回の仮想DOMと突っつき合わせて差分を計算して、差分のみを描画するっていう戦略を採っている。
;;わざと冗長な書き方をしている ;;アプリの状態を保持するref (def state (atom {:articles [{:id "xxx" :title "yyy" :text "zzz"}] :selected nil})) ;;3 (defn render-all [state] [:div [:h1 "ブログ"] [:div#articles (map (fn [atcl] [:div [:h2.article-title {:data-id (:id atcl)} (:title atcl)] (when (= (:selected state) (:id atcl)) [:p (:text atcl)])) (:articles state))]) (def vdom (atom (render-all @state))) ;;1 & 2 ;;記事のタイトルをクリックされたら記事idを設定する (.addEventListener (.querySelector js/document "#articles") "click" (fn [e] (let [id (.getAttribute (.-target e) "id") st (swap! state assoc :selected id)] (swap! vdom render-diff (render-all st)))))
仮想DOMが無いと、状態からUIを生成する部分を純粋関数として書くのは難しくなるけど、うまくやれば、状態からUIが一意に決まるという性質は残せる。
Pasta(deprecated)とkakahiakaは、仮想DOMを持たない実装で、つまりDOMの上でなくても使える。ミュータブルなオブジェクトで表現されたUIの世界の上に純粋関数としてのアプリケーションを構築するためのものって言えるとこまで汎用化してある。
どうやっているかっていうと、差分を出す部分を1段階早くしていて、2のアプリ関数は新しい状態を返す代わりに現在の状態への差分を返すようにしている。で、3は状態の変更をオブザーブしていて、新しい状態と古い状態を渡されるからそれを使って命令的に描画をする。
var K = window.kakahiaka; //アプリの状態を保持するref var app = K.app({ articles: [{id: "xxx", title: "yyy", text: "zzz"}], selected: null }); //3 K.watch_transition(app, "selected", function (state, old_state) { $('.article[data-id=' + old_state.selected + '] p').hide(); $('.article[data-id=' + state.selected + '] p').show(); }); //2 var selectArticle = K.deftransition(function (state, id) { return {selected: id}; //トランジションは状態と引数を受け取って差分を返す純粋関数 }); //1 $("#articles").on("click", ".article-title", function () { selectArticle(app, $(this).data("id")); //トランジションの起動 });
これだと、純粋関数として書けるのは2だけなんだけど、アプリケーション自体が純粋関数としての性質を持つのは変わらなくて、状態の保存->復元とか、undo,redoとかバグレポートに添付された状態の読み込みとかしたら勝手にUIも復元されるようになるので便利。
htmlは全く汚れないし、jsでhtmlを生成するわけでもないのでhtmlコーダとフロントエンドエンジニアの分業が楽。逆に一人で両方やるなら仮想DOMとかどんどん使うと良い。
まとめ
アプリケーション全体を眺めたときに、
- イベントが起きるたびに状態を更新する純粋関数が起動される
- 差分描画の仕組みがある
- 状態からUIが一意に決まる
こういう構造(まあ要するにFluxとかREPL)になっていると幸せっすよ。っていうのと、
FluxにはMVCがどうとかじゃなくてこういう道筋でも至れるんだぜってことと、
プログラムの一部が純粋関数型でなくてもそんなに気に病まなくてもいいかもね
ってことが言いたかった。
以前の記事: