Elm Architectureとその特徴
いよいよアプリケーションを作りたいところですが、何か指針がないと、秩序のあるアプリケーションをスムーズに構築することはできません。
そこで、Elmでは「Elm Architecture」と呼ばれる設計手法に従うことが強く推奨されています。 これは、他のJavaScriptフレームワークにおけるMVCやFlux[1]といった概念に相当します。 さっそく中身を見ていきましょう。
一方向のデータフロー
従来のアプリケーションでは各UIコンポーネントに状態を持ち、それを必要に応じて取り出して使うという方法を採っていましたが、規模が大きくなるとすぐにどこで何をしているのか分からなくなってしまいます。
Elm Architectureではデータフローを一方向に保ちます。 具体的には、ユーザアクションやサーバイベントを起点にして、アプリケーション内部に保持するデータを更新し、最終的にそのデータが画面描画のロジックに流れていきます。
Model・Update・View
Elm Architectureでは、プログラムを大きくModel、Update、Viewという3つのセクションに分けて記述します。以下、モデルやビューといった用語は良く知られたMVCアーキテクチャの用語を流用しますので、説明不足を感じられましたらご容赦ください。また、クリックなどのイベントをここではアクションと呼んでいます。
セクション名 | 役割 |
---|---|
Model | プログラム中で使うモデルを定義する |
Update | アクションを受け取り、モデルの更新を行う |
View | 画面描画とアクションの発行を行う |
最も参考になるのは、Elmで書かれたTodoアプリでしょう[2]。 全体が1枚のファイルに書かれているため、上から下まで目を通すだけで雰囲気がつかめると思います。
Elm Architectureを使った実装の基本パターン
次に、Elm Architectureを使ってアプリケーションを実装するときの基本パターンを見てきます。 ただし、「Elmで書かれたTodoアプリ」はここでの説明には大きすぎるため、もう少し小さいサンプルを用意しました。
ステップ1:Model・Update・Viewを定義する
次のサンプルは「ボタンを押した回数をボタン自身に表示する」というものです。
import Html exposing (..) import Html.Attributes exposing (style) import Html.Events exposing (onClick) {- TODO: あとでここを埋めます -} -- MODEL type alias Model = { count : Int } init : Model init = { count = 0 } -- UPDATE type Action = NoOp | Increment update : Action -> Model -> Model update action model = case action of NoOp -> model Increment -> { model | count <- model.count + 1 } -- VIEW view : Signal.Address Action -> Model -> Html view address model = button [ onClick address Increment , style [("width", "100px"), ("height", "100px"), ("font-size", "large")] ] [ text (toString model.count) ]
【コードの解説】
- MODELでは、このアプリケーションで扱う全ての状態の型と初期値を定義しています。
- UPDATEでは、このアプリケーションで扱う全アクションと、それぞれのアクションに対して状態を更新する関数を定義しています。更新といっても、Elmの変数は全てImmutableなので、古い値を受け取って新しい値を返す関数になっています。
-
VIEWでは、状態を入力としてHTMLを生成する関数を定義しています。ここでは、実際にDOMオブジェクトを生成しているわけではなく、DOMに見立てた仮想的なオブジェクト(Virtual DOM。後述のコラムを参照)を生成しています。VIEWで生成するHTML要素は全て
要素名 [属性] [子要素]
という関数になっているため、直感的に記述していくことができるでしょう。
ところで、VIEWセクションにaddress
という変数が登場しています。 これは、メールアドレスと同じように、アクションを発行する際の「あて先」を表しています。 view
関数の第1引数であるSignal.Address Action
という型は、「Action
型を受け付けるAddress
」を表しています。
アクションは要素の属性値として記述します。 onClick address Increment
という記述は、クリック時にIncrement
というアクションをaddress
に対して発行することを意味しています。 イベントやその他の属性の定義についてはevancz/elm-htmlを参照してください。
ステップ2:Signalを使って動作させる
さて、主要な関数は定義できましたが、main
すらないこのサンプルは、まだ動きません。 {- TODO: ... -}
になっている部分を埋めていきましょう。
actions : Signal.Mailbox Action actions = Signal.mailbox NoOp state : Signal Model state = Signal.foldp update init actions.signal main : Signal Html main = Signal.map (view actions.address) state
【コードの解説】
-
actions
はメールの受信箱です。先ほど説明したAddress
宛てに発行されたAction
はここに到達するわけです。ここに届いたAction
は、actions.signal
でSignalとして取り出すことができます。ここでは、初期値としてNoOp
を設定しています。 -
state
は、常に最新の状態を持ったSignalです。actions.signal
を入力、init
を初期値として、Signal.foldp
関数を使ってを常に最新の状態に更新していきます。 -
main
では、Signal Model
からSignal Html
への変換を行い、常に最新の状態であるstate
から最新の画面を生み出しています。
以上で、晴れてカウンタが動くようになりました。 ボタン1個しかない割にはたくさんのコードを書き連ねた感がありますが、この作業が必要になるのは最初の1回だけです。 以降は、機能を追加するときにModel・ Update・Viewの必要なところへコードを足すだけで済みます。
ステップ3:必要に応じて共通化する
ステップ2のコードをよく見ると、アプリケーション固有の情報がどこにも書かれていないことに気づくでしょう。 つまり、この部分は別の画面にも使い回すことができます。
実際にこの共通化を行っている「start-app」というライブラリがあるので、これを使って書いてもよいでしょう。 {- TODO: ... -}
の部分を次のように書き直しても同じように動きます。
import StartApp.Simple as StartApp main = StartApp.start { model = init, view = view, update = update }
この方法は手軽ですが、これではまかない切れないパターンも出てきます。そのときにはあきらめて、自分で書きましょう。 プロジェクト固有のパターンを作るなどしてもよいと思います。
基本パターンのまとめ
ここまでを図式にすると次のようになります。
Elm Architectureに従うと、全体で必要なSignalはたった3つです。
ElmとVirtual DOM
もしかすると、VIEWのロジックに違和感を覚えた方がいらっしゃるかもしれません。 コードを見る限りでは、何かアクションがあるたびにHTMLを上から下まで再描画しているように見えます。 DOMの描画コストが高いことを知っている方であれば、有無を言わさず避けたいコードでしょう。
しかし、ElmではVirtual DOMという仕組みを使うことによって、高速な描画を実現しています。
簡単に説明しましょう。 Virtual DOMとは、DOMを模した構造体です。 VIEW部分でイベントのたびに生成されるのは、この偽物のDOM(Virtual DOM)です。 ポイントは、Virtual DOMの生成コストが、本物のDOMに比べるとかなり低いことです。そのため、Virtual DOMであれば画面全体を作り直しても大したコストになりません。 そして、本物のDOMには、新旧のVirtual DOMから抽出された差分のみを反映します。 これが、高速な描画を実現するVirtual DOMの仕組みです。
Virtual DOMはReact.jsによって発明されたもので、そちらにも同じ説明があります。
しかし、Elmの場合には続きがあります。 Elmの言語特性である「純粋性」と「不変性」が、さらに高速な描画を可能にするのです。
状態に依存しない純粋な関数であれば、同じモデルが生成するビューは常に同じです。 つまり、モデル同士を比較してしまえばVirtual DOMすら作る必要がありません。 また、不変なデータ構造を持つことで、新旧のモデルの比較も容易になっています。
ElmのVirtual DOMに関しては、Elm公式のアナウンス「Blazing Fast HTML(邦訳: 爆速HTML – Elmでの仮想DOM)」に詳しい説明があります。ぜひ参照してみてください。