SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

Webアプリケーション開発技術の新潮流スタディーズ

関数型リアクティブプログラミング言語Elmに学ぶ フロントエンド開発の新しい形 【後編】

Webアプリケーション開発技術の新潮流スタディーズ 第5回


  • X ポスト
  • このエントリーをはてなブックマークに追加

ダウンロード samples.zip (8.2 KB)

非同期通信を発生させるコンポーネント

さて、ここからが本題です。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)をダウンロードしてください。

Counter.elm
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 ]
Main.elm
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は外部からは詳細を知らずに対応できるようになっています。

EffectsをElm Architectureに組み込むイメージ
EffectsをElm Architectureに組み込むイメージ

* * *

全2回にわたってElmの仕組みとアプリケーションの作り方について解説しました。 ここまでで、基本的なアプリケーションならば一通り作れるようになっています。 より現実的な問題(例えばJavaScriptライブラリやブラウザAPIとの連携など)については説明を省いているため、公式サイトや他のElmに関する記事を参考にしてください。

Elmはまだまだ進化を続けています。多少こなれていない機能も今後改善していくでしょう。 Webフロントエンド開発の新しい選択肢の1つとして、ぜひ検討してみてください。

修正履歴

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
Webアプリケーション開発技術の新潮流スタディーズ連載記事一覧

もっと読む

この記事の著者

鳥居 陽介(株式会社ワークスアプリケーションズ)(トリイ ヨウスケ)

株式会社ワークスアプリケーションズ所属。イケてるアプリケーションを死ぬほど楽に作るために研究を続ける日々。社内での立ち位置は「フロントエンドのナウい人」。最近エバンジェリストという肩書きが付いた。趣味は作曲とスノーボード。 Blog: http://jinjor-labo.hatenablog.com/ ...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/8986 2017/03/02 16:39

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング