非同期通信を発生させるコンポーネント
さて、ここからが本題です。TaskをElm Architectureに組み込むにはどうすればよいでしょう。 先ほどの例では、初期化時に1回だけ、データ取得のために通信していましたが、ユーザアクションを引き金にして通信が発生するケースはよくあるでしょう。 ここでは、ボタンを押したときにサーバからデータを取ってくるようなケースを考えます。
いきなり答えを出す前に、まずは今まで使ってきた道具を用いて実現できないかを考えてみます。 アクションを引き金にしてTaskが発生するので、update
関数で処理するのが適切でしょう。 そこで、update
関数がTaskを返すようにします。すると、次のようになります。
update : Address Action -> Action -> Model -> (Model, Maybe (Task x Action)) update address action model = case action of Load -> let task = getData `andThen` (\data -> Signal.send address (Data data)) in (model, Just task) -- Taskがある場合 Data data -> ({ model | data <- data }, Nothing) -- Taskがない場合
Load
というアクションを引き金にしてデータを非同期で取得し、その結果を今度はData
というアクションとして受け取るようにしています。 結果がタプルになるのは仕方ないとしても、Addressが登場するなど、少々複雑になってしまいました。
Effectsの導入
Taskの負担を和らげるために、最近、Elmに「Effects」というモジュールが導入されました。 Effectsは「evancz/elm-effects」パッケージで提供されています。
先ほどのコードをEffectsを使って書き直すと、次のようになります。
update : Action -> Model -> (Model, Effects Action) update action model = case action of Load -> (model, Effects.task (Task.map Data getData)) -- TaskをEffectsに変換 Data data -> ({ model | data <- data }, Effects.none) -- Effectsがない場合
Addressがなくなってスッキリしました。 Elm ArchitectureではAddress(を提供するMailbox)はシングルトンであるため、Addressを指定する手間を省略しているのです。 EffectsはAddressを後から与えられることで、Taskに還元されます。
toTask : Address (List a) -> Effects a -> Task Never ()
先ほど少し触れた「start-app」ライブラリを使うと、この変換処理は完全にライブラリに預けることができます。
サンプル:押すとデータを取得するボタン
以下は、「ボタンを押すと、サーバから取得したデータがボタン自身に表示される」サンプルの全ソースです。update
関数の定義が変わっていることと、「start-app」内でEffectsからTaskへの変換が行われ、それをportしているところに注目してください。
ここでは簡単にするために、ボタンの数を1つに戻し、マウスオーバーの処理も除いています。完全版は本記事のサンプル(sample.zip)をダウンロードしてください。
module Counter where import Html exposing (..) import Html.Attributes exposing (style) import Html.Events exposing (onClick) import Task exposing (Task, onError) import Effects exposing (Effects) -- MODEL type alias Model = { data : String } init : Model init = { data = "(click me!)" } -- UPDATE type Action = Load | Data String update : Action -> Model -> (Model, Effects Action) update action model = case action of Load -> -- データを取得し、コールバックをDataアクションとして発行する (model, Effects.task (Task.map Data getData)) Data data -> ({ model | data <- data }, Effects.none) getData : Task x String getData = Task.succeed "This is the data from the server" -- VIEW view : Signal.Address Action -> Model -> Html view address model = button [ onClick address Load -- クリック時にLoadアクションを発行 , style [ ("width", "100px") , ("height", "100px") , ("font-size", "large") ] ] [ text model.data ]
import Html exposing (..) import Html.Attributes exposing (style) import Counter import Task exposing (Task) import Effects exposing (Effects, Never) import StartApp -- Effectsに対応した(Simpleではない方の)StartAppを使う app = StartApp.start { init = init , update = update , view = view , inputs = [] } main = app.html -- StartAppがEffectsをTaskのSignalに変換してくれるので、動作させるためにportする port tasks : Signal (Task.Task Never ()) port tasks = app.tasks -- MODEL type alias Model = { title : String , counter : Counter.Model } init : (Model, Effects Action) init = ({ title = "Simple Data Loader" , counter = Counter.init }, Effects.none) -- 最初のEffectsはなし -- UPDATE type Action = NoOp | CounterAction Counter.Action update : Action -> Model -> (Model, Effects Action) update action model = case action of NoOp -> (model, Effects.none) CounterAction action -> let -- update関数の結果をタプルで受け取り、展開する (newCounter, eff) = Counter.update action model.counter in ({ model | counter <- newCounter }, Effects.map CounterAction eff) -- ActionをCounterActionでラップ -- VIEW view : Signal.Address Action -> Model -> Html view address model = div [] [ h1 [] [text model.title] , Counter.view (Signal.forwardTo address CounterAction) model.counter ]
ここでもやはりActionは階層化され、コンポーネントの発行したActionは外部からは詳細を知らずに対応できるようになっています。
* * *
全2回にわたってElmの仕組みとアプリケーションの作り方について解説しました。 ここまでで、基本的なアプリケーションならば一通り作れるようになっています。 より現実的な問題(例えばJavaScriptライブラリやブラウザAPIとの連携など)については説明を省いているため、公式サイトや他のElmに関する記事を参考にしてください。
Elmはまだまだ進化を続けています。多少こなれていない機能も今後改善していくでしょう。 Webフロントエンド開発の新しい選択肢の1つとして、ぜひ検討してみてください。