SHOEISHA iD

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

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

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

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

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

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


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

Elm Architectureの応用 ⑴:コンポーネントを作る

次は、何かUIコンポーネントを作ってみましょう。 ここでは、先ほど作ったカウンタを、再利用可能なモジュール(Counterモジュール)として切り出してみます。

Counterモジュールを作る

そういえば、モジュールを作る方法をまだ説明していませんでしたね。 先ほどの例のステップ1で書いた部分を、Counter.elmに切り出してみましょう。

Counter.elm
module Counter where -- モジュールの宣言

import Html exposing (..)
import Html.Attributes exposing (style)
import Html.Events exposing (onClick)

-- MODEL
(Main.elmからカット&ペースト)

-- UPDATE
(Main.elmからカット&ペースト)

-- VIEW
(Main.elmからカット&ペースト)

ここに定義された関数は、Counter.関数名で外部から使用することができます。

MainモジュールもMODEL・UPDATE・VIEWにする

それでは、Main.elmからカウンタ(Counter.elm)を利用してみましょう。 Main.elmから先ほどのCounterモジュールをインポートし、メイン画面のために新たにMODEL・UPDATE・VIEWを定義します。

Main.elm
import Html exposing (..)
import Counter -- モジュールのインポート

{- actions, state, main は変わらないため省略 -}

-- MODEL

type alias Model =
  { title : String
  , counter : Counter.Model -- CounterのModelをMainのModelでラップする
  }

init : Model
init =
  { title = "My Counter"
  , counter = Counter.init
  }

-- UPDATE

type Action
  = NoOp
  | CounterAction Counter.Action -- CounterのActionをMainのActionでラップする


update : Action -> Model -> Model
update action model =
  case action of
    NoOp -> model
    CounterAction action ->
      { model |
        counter <- Counter.update action model.counter
      }

-- VIEW

view : Signal.Address Action -> Model -> Html
view address model =
  div
    []
    [ h1 [] [text model.title]
    , Counter.view (Signal.forwardTo address CounterAction) model.counter
    ]

それではこのコードの内容を、順に見ていきましょう。

【コードの解説】

  • MODELでは、Main自身の状態(タイトル)とCounterコンポーネントの状態を持ったModel型を定義し、初期値を宣言しています。
  • UPDATEでは、Counter.Action型をCounterActionでラップしたAction型を定義しています。さらに、update関数では、カウンタからのアクションを受け取り、カウンタの状態を更新しています。
  • VIEWでは、タイトルとカウンタを描画しています。Signal.forwardTo定義)はAddressを変換するための関数で、ここではカウンタから受け取ったModel.Action型のアクションをCounterActionでラップするように指示しています[3]

見てのとおり、CounterモジュールとMainモジュールの構造はまったく同じになっています。 これはElm Architectureの最も大きな特徴の1つといえるでしょう。 どの階層でも、MODEL・UPDATE・VIEWだけを考えてコーディングし、あとは適切に入れ子にするだけです。

コンポーネントをネストするイメージ
コンポーネントをネストするイメージ

[3]: Union Typeも関数と同じように部分適用することができます。CounterAction Counter.ActionAction型ですが、CounterActionCounter.Action -> Action型となります。

カウンタを増やしてみる

今度は「カウンタを4つに増やし、さらにその合計を表示する」ということをしてみます。

Main.elm
-- MODEL

type alias Model =
  { title : String
  , counters : List (ID, Counter.Model) -- それぞれIDとペアにする
  }
type alias ID = Int

init : Model
init =
  { title = "My Counter"
  , counters = List.indexedMap (,) (List.repeat 4 Counter.init)
  }

-- UPDATE

type Action
  = NoOp
  | CounterAction ID Counter.Action


update : Action -> Model -> Model
update action model =
  case action of
    NoOp -> model
    CounterAction id action ->
      let
        -- IDが一致するものを更新
        update' (id', counter) =
          if id == id'
            then (id', Counter.update action counter)
            else (id', counter)
      in
        { model |
          counters <- List.map update' model.counters
        }

-- VIEW

view : Signal.Address Action -> Model -> Html
view address model =
  let
    viewEach (id, counter) =
      Counter.view (Signal.forwardTo address (CounterAction id)) counter
    -- 合計を計算
    sum = List.sum (List.map (\(id, counter) -> counter.count) model.counters)
  in
    div
      []
      [ h1 [] [text model.title]
      , div [] (List.map viewEach model.counters)
      , div [] [text ("total: " ++ toString sum)]
      ]

先ほどとの違いは、異なるカウンタ同士を区別するためにIDを割り振っている点です。 その分だけコードのかさが増してはいますが、無事に合計値を計算することができました。

カウンタに機能を追加する

ところで、個々のカウンタの状態は、コンポーネントの外側、つまりMain.elmで管理されています。 「コンポーネントの状態を外で管理する」と聞くと、コンポーネントに機能を追加するたびに外部で扱うコードが増えていきそうですが、実際にはそうはなりません。 先ほどの例ではカウンタの数字を1つずつ増やしていましたが、どういうアクションが起こったとき(例えばクリック)にどのように増やすか(例えば+1する)という情報は、外からは全く見えません。それらは全てコンポーネントのモジュール内に書かれています。 そのため、コンポーネントの使用者は「Actionが来たらupdateする」ということだけ覚えておけば、その具体的な内容を知る必要はないのです。 Elm流のカプセル化と言えるかもしれません。

実装の詳細が隠蔽されていることを検証するため、カウンタに1つ機能を追加してみましょう。 次の例では、マウスカーソルが乗ったらボタンの色を変えるようにしています[4](変更箇所を★で示しています)。

Counter.elm
module Counter where

import Html exposing (..)
import Html.Attributes exposing (style)
import Html.Events exposing (onClick, onMouseEnter, onMouseLeave) -- ★

-- MODEL

type alias Model =
  { count : Int
  , hover : Bool -- ★
  }

init : Model
init =
  { count = 0
  , hover = False -- ★
  }

-- UPDATE

type Action
  = NoOp
  | Increment
  | Hover Bool -- ★

update : Action -> Model -> Model
update action model =
  case action of
    NoOp -> model
    Increment ->
      { model |
        count <- model.count + 1
      }
    Hover hover ->
      { model |
        hover <- hover
      } -- ★

-- VIEW

view : Signal.Address Action -> Model -> Html
view address model =
  button
    [ onClick address Increment
    , onMouseEnter address (Hover True) -- ★
    , onMouseLeave address (Hover False) -- ★
    , style
        [ ("width", "100px")
        , ("height", "100px")
        , ("font-size", "large")
        , ("background", if model.hover then "#abc" else "") -- ★
        ]
    ]
    [ text (toString model.count) ]

いかがでしょうか。 Counter.elmは書き換えていますが、Main.elmは全く書き換えていません。 カウンタの使用者は、ActionHover Boolが追加されたことも、Modelにhoverが追加されたことも知らないまま、カウンタを使い続けることができます。

[4]: もちろんCSSでもできますが、ここでは置いておきましょう。

コンポーネント作成の特徴と考察

  • コンポーネントと、その使用者側にそれぞれMODEL・UPDATE・VIEWを定義する
  • コンポーネントは自身に状態を持たない。状態は外(使用者側)で管理し、コンポーネントから発行されたActionをコンポーネント自身にフィードバックする必要がある
  • このとき、コンポーネントの使用者はコンポーネントの実装の詳細を気にする必要がない

これらのうち、2番目の特徴は、いわゆるオブジェクト指向的なコンポーネントに慣れているとわずらわしく感じるかもしれません。 カウンタをクリックしたときにカウンタ自身の数値を増やすのだから、わざわざカウンタの外部にアクションを発行しなくてもよいような気もします。 しかし、最初は内部に閉じていたアクションが後に外部で必要になったとき、onXXX()のようなコールバックが次々と必要になるでしょう。 そうではなく、VIEWを純粋な関数にし、描画に必要なデータを全て外から渡してもらうようにすることで、アプリケーションの構造をシンプルに保つことができます。

一風変わったコンポーネントの作り方を見たところで、次ページでは非同期通信を扱う方法を見ていきます。

次のページ
Elm Architectureの応用 ⑵:非同期通信の導入

修正履歴

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

Webアプリケーション開発技術の新潮流スタディーズ連載記事一覧

もっと読む

この記事の著者

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

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

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

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

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/8986 2017/03/02 16:39

イベント

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

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

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

メールバックナンバー