ElmでTodoアプリを作ってみる(2)

May 16th, 2020

Introduction

前回の記事 ちょっと仕事詰め込みすぎてずっと働いてました、、、 というのは言い訳で、本当はRustのチュートリアルこなしたりScalaの本読んだり、仕事以外でInputの時間は結構あったけど、やりかけを思い出すのってなんか面倒で放置したら半年以上たってしまいました。

前回、JsonエンコードやLocal Storageアクセスらへんについて説明したので、今回は前回説明しきれなかった

  1. Model
  2. Updata
  3. Message
  4. elm-css

らへんを説明していければと思います。

Elmにおける状態管理とイベント

ElmにはElmアーキテクチャという思想があり、基本的にElmアプリケーションは

  • Model
  • View
  • Update

を定義して構築します。 ReduxやVuexはElmアーキテクチャの影響を受けているので、Elmに手を出すような人はこれらを学んできた人も多いと思うので、理解しやすいかもしれません。

Model

Modelはサイト内における状態を管理しています。

-- MODEL


type alias Todo =
  { title : String
  , date : Time.Posix
  }


type alias Model =
  { zone : Time.Zone
  , time : Time.Posix
  , todos : List Todo
  , userInput : String
  }

今回のTodoでは時計も持っているので、タイムゾーンと時間、Todo、入力中文字列を状態として扱うことになります。

init

initはElmアプリケーションを起動した際に実行され、Modelの初期値や起動時に必要な調整処理を発行したりします。

init : D.Value -> (Model, Cmd Msg)
init flags =
  ( Model Time.utc (Time.millisToPosix 0) (todosDecode flags) "test"
  , Task.perform AdjustTimeZone Time.here
  )

flagsは前回ちょっと書きましたが、JS側でElm起動時に引数で渡せます。 なので、今回はLocal Storageから取得したTodo情報がinitに渡されます。 これを元に、Modelの初期値とタイムゾーン調整を促すタスクの発行をCmdというElm外部とのやりとり用の仕組みを使ってやっています。

Update

次にUpdateですが、UpdateはMessageと呼ばれる、Reduxで言う所のactionに応じてModelを更新する、いわばReducerに当たる処理を記述します。

この辺は本当にReduxそっくりです。

-- UPDATE


type Msg
  = Tick Time.Posix
  | AdjustTimeZone Time.Zone
  | Add String
  | Delete Time.Posix
  | Input String


update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    Tick newTime ->
      ( { model | time = newTime }
      , Cmd.none
      )

    AdjustTimeZone newZone ->
      ( { model | zone = newZone }
      , Cmd.none
      )

    Add input ->
      let
        newModel =
          { model | todos = ( Todo input model.time ) :: model.todos, userInput = "" }
      in
      ( newModel
      , Ports.save (todosEncode newModel.todos)
      )

    Delete date ->
      let
        newModel = { model | todos = filter (isOldTodo date) model.todos }
      in
      ( newModel
      , Ports.save (todosEncode newModel.todos)
      )

    Input input ->
      ( { model | userInput = input }
      , Cmd.none
      )

Elmを初めてみた人でもわかるくらい、Reduxそっくりじゃないですかね? AddとDeleteでTodoのListに新しいTodoを追加したり、時間からfilterして特定のTodoを削除したりして、新しいModelを作成しています。 それをまたCmdを使って外部へ保存命令を出しています。

Message

Reduxでいうactionに当たるのがMessageだと言いましたが、ではMessageはどうやって発行すればいいんでしょう? これまた結構簡単で、domのonclick属性とかに発行してほしいMessageの情報を渡すだけでおっけーです。

viewForm : String -> Styled.Html Msg
viewForm input =
  Styled.form
    [ onSubmit (Add input)
    , css
      [ marginTop (px 30)
      , color (hex "ccc")
      , fontSize (px 16)
      , lineHeight (px 16)
      ]
    ]
    [ Styled.p [] [ Styled.text "Write your new Todo." ]
    , Styled.input
      [ onInput Input
      , value input
      , css
        [ backgroundColor transparent
        , borderBottom3 (px 1) solid (hex "fff")
        , color (hex "fff")
        , fontSize (px 20)
        , lineHeight (px 20)
        , padding (px 10)
        , width (px 500)
        ]
      ]
      []
    ]

上記のonInput Inputといいところがユーザーの入力値をMessageにしているところですね。 onInputはMessageを受け取って、ユーザーがinputしたらそのMessageを入力値とともに発行してくれます。 JSだとどんな関数でも実行できちゃうので人による好みとかパターンがいろいろ出てくるんですが、Elmのこのアプローチはシンプルだし、人による実装差異も生まれにくいわけです。

同じような感じで、Todoの更新と削除のMessageも簡単に実装できます。

viewList : List Todo -> List (Styled.Html Msg)
viewList todos =
  case todos of
    [] ->
      []

    _ ->
      [ Styled.div
        [ css
          [ marginTop (px 30)
          ]
        ]
        [ Styled.ul
          [ css
            [ displayFlex
            , flexWrap wrap
            , marginTop (px -20)
            , marginLeft (px -20)
            ]
          ]
          <| ( todos
            |> List.map (\todo -> Styled.li
              [ css
                [ boxSizing borderBox
                , displayFlex
                , alignItems center
                , justifyContent spaceBetween
                , backgroundColor (hex "fff")
                , borderRadius (px 3)
                , boxShadow4 (px 0) (px 4) (px 24) (rgba 0 0 0 0.15)
                , color (hex "aaa")
                , fontSize (px 25)
                , padding3 (px 40) (px 20)  (px 20)
                , height (px 200)
                , width (px 200)
                , marginTop (px 20)
                , marginLeft (px 20)
                , position relative
                , transform (translateY (px 0))
                , transition
                  [ Css.Transitions.boxShadow 500
                  , Css.Transitions.transform 500
                  ]
                , hover
                  [ boxShadow4 (px 0) (px 4) (px 48) (rgba 0 0 0 0.3)
                  , transform (translateY (px -3))
                  ]
                ]
              ]
              [ Styled.text todo.title,
                Styled.button
                [ onClick (Delete todo.date)
                , css
                  [ position absolute
                  , top (px 20)
                  , right (px 20)
                  ]
                ]
                [ Styled.img
                  [ src "src/image/icon.png"
                  , css
                    [ width (px 20)
                    ]
                  ]
                  [
                  ]
                ]
              ]
            ))
        ]
      ]

ほとんどスタイルに関する設定で、onClickとかは1行ずつしかないですね。

Elmにおけるスタイルやシンタックス

elm-css

実はこれ前回の記事にも書いたんですが、大事なことなのでもう一回言おうとお思いますw elm-cssを使うとCSSにも型を強制することができます。

まぁブラウザで見てるんだから、実際に本番環境に壊れたスタイル属性が行くことってまぁほぼないとは思うんですが、とはいえ誤字で動かないとか指定の補完がないとか、Typescriptとかでなれた方なら不満でいっぱいな部分じゃないかと思います(僕は不満です)。

ということで、これだけでもElm使う価値があるんじゃないかくらいやってみたら嬉しかった部分なので、Elm触ってみる人はぜひこれがおすすめです。

if式やパターンマッチ

モダンな言語なら結構サポートしてると思いますが、if式やパターンマッチはやっぱり素晴らしいですね。 パターンマッチはEcma Scriptでproposal出てるからそのうちJSもサポートするんだろうけど、好みとしてはElmやRustのパターンマッチの書き方が今時っぽくて好きですね。

その他のシンタックス

再代入不可、デフォルトでカリー化されてること、あとHaskellに似たこの独特なインデントの仕方が慣れてくるとすばらしいですね。 RustやScalaも関数型言語の流れを汲んでいますが、こんくらい徹底されてるのはやはり一味違うなぁと思います。

まとめ

ソースは以下で公開してます。 https://github.com/AkifumiSato/elm-todo/pull/1/files

ELmはやはり、チーム開発で利用するには一般認知度的にもまだまだ低いし、フロントエンド特化な言語なので今後も趣味以外でいじることはなさそうな気がしてます。 ただやはり最近の言語は関数型のシンタックスや流れを汲む傾向にあるので、ElmやHaskellを学ぶことはフロント・サーバーサイド問わずメリットは大きいと思います。

願わくば、僕の予想に反してElmの案件とかがふえていくといいなぁ、、、