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

September 18th, 2019

Introduction

前の記事からずいぶん空いてしまいましたが、ずっと興味のあったElmについてTodoやってみました。

まだ制作途中ですが、なんとなく少し形になってきたので、途中ながら備忘録がてら躓きポイントをまとめたので、初めての方の参考になればと思います。

今回作るものの最終形は↓こんな感じです。

https://akfm-elm-todo.netlify.com/

Elm is ?

まずElmについて。 Elmは関数型言語です。

「ElmってほぼHaskellでしょ?」とか言われるんですが、ElmはHaskellより言語としてはかなりライトだと思います。 実際0.19までで初期にあったパラダイム(当初はFRPの本命とか言われたりしてたみたいですね)や機能も削られ、どんどんシンプルになっていってるらしいです。 あとHaskellはシンタックスも数学的アプローチの手法も多く、比較するとElmは小難しい知識なしにいじれるようにはなってる気がします。

その分、Elmでやれることに不満を持った方にはPureScriptという変態言g…すばらしいaltJSがあるので、こちらを試してみるといいと思います。

何はと思われまずはTutorial

ここではTodoアプリを作っていくにあたり初歩的な部分や細かい実装の解説はしないので、まだやっていない人はTutorialをこなすことをお勧めします。

https://guide.elm-lang.jp/

特に今回は

  • JSON
  • 時間取得
  • JSとのやりとり
  • 外部CSSの読み込み

などが発生するので、Tutrorialの最後の方ですがちゃんとソース全部読みたい方は必見です。 この辺も全部わかってない状態で私はスタートしたので理解するのに苦労しました、、、

Todoアプリの構成

Todo情報の格納先はAPIとか用意したくなかったのでlocalSorageにして、portでJSとつないでいます。 あと今回は後半もっかい書きますが、Css in JS的な感じにやっぱしたかったのでrtfeldman/elm-cssを使用しています。

スタイル部分も完全に型の恩恵受けられるのは素晴らしいですね。 なんかもう、HtmlもCssも全部型欲しくなってくる。。。

ちなみにビルド処理はnpm scriptにして、ElmのビルドはJSだけにして

elm make src/Main.elm --output=build/elm.js

みたいな感じでJSだけビルドする形にしました。 ただ毎回ビルドするのは面倒なんで、開発中はelm-live使ってました。

雛形の作成

正直ほとんどTutorialの途中みたいなの拾えばやれるので、ここでは省略します。

ということで、完成形の全体像。

https://github.com/AkifumiSato/elm-todo/tree/6638102abc354a77073bf4a8fb5ba9901567307c

↑を作るにあたり、躓いた部分をいくつか解説しようと思います。

まずJS側

const app = Elm.Main.init({
  node: document.getElementById('elm'),
  flags: JSON.parse(localStorage.getItem('NxTodo')),
})
app.ports.save.subscribe(function(data) {
  localStorage.setItem('NxTodo', JSON.stringify(data))
})

今回はJSONとやり取りするので、portとflagsを使って初期読み取りとupdate時のAPIを定義しました。 JS側でAPIを用意できるのでブラウザで用意するAPIの進化にも対応できるし、Elmは本当によくできた言語ですね(信者)。

Elm側のJSONデコード処理

Elmの全体部分はあとでGithubのリンク貼るので躓いた箇所を中心に解説します。 まずflags受け取ってデコードする部分。

-- ...
import Task
import Time
import Json.Decode as D
-- ...

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


todosDecode : D.Value -> List Todo
todosDecode flags =
  let
    decoder =
      D.list
        <| D.map2 Todo
          (D.field "title" D.string)
          (D.field "date"
            <| D.map (\val -> Time.millisToPosix val) D.int
          )
    result = (D.decodeValue decoder flags)
  in
  case result of
    Ok todos ->
      todos
    Err _ ->
      []

もうちょっと綺麗に書ける気もしてるのですがあしからず、、、

D.Valueというデコード可能な値を受け取ってデコード処理をしてます。 TimeやTaskはTutorialに解説は任せて、ここのデコード処理のネスト部分がネックでした。

デコードするにはまずDecoderというものが必要で、最終的にはD.decodeValue decoder valueと渡せればデコード処理がされます。 ただこのDecoderを作るには、プリミティブだったらいいんですが今回はTodoがTime.Posixを保持してるので変換処理を必要としています。

D.map (\val -> Time.millisToPosix val) D.int

ここがまさにその変換処理ですね。 D.mapでD.intでデコードした値をさらに変換しています。

オブジェクト形式部分はD.map2、配列部分はD.listで対応しています。

ElmのJSONエンコード処理

次は逆に保存時のエンコード処理です。

-- ...
import Json.Encode as E
-- ...
todosEncode : List Todo -> E.Value
todosEncode todos =
    E.list
      E.object
      ( todos
        |> List.map (\todo ->
          [ ( "title", E.string todo.title )
          , ( "date", E.int (Time.posixToMillis todo.date) )
          ] )
      )

なんかデコード処理と似てるには似てますね。 強いて言えばもうちょっとデコード側と合わせるように書けばよかったかな、、、

ElmのCss in JS

一部ですがElmでCSS in JS的な感じで使えるライブラリとして先述したrtfeldman/elm-cssを使用しているんですが、それを用いてComponent化(関数化)すると↓こんな感じになります。

viewHeader : String -> Styled.Html Msg
viewHeader time =
  Styled.header
    []
    [ Styled.h1
      [ css
        [ color (hex "fff")
        , fontSize (px 30)
        ]
      ]
      [ Styled.text "NxTodo" ]
    , Styled.p
      [ css
        [ color (hex "fff")
        , fontSize (px 100)
        , lineHeight (px 100)
        , marginTop (px 30)
        ]
      ]
      [ Styled.text (time) ]
    ]

スタイルをテキストで各部分はなくなり、許容できない型はエラーとなります。 アニメーションとかちょっと癖強かったけど、慣れてくるとこれはCssの理想形なのでは、、、って気になってきます。

Main.elmの全体像

執筆時点でのElmのMain.elmは↓こんな感じです。

https://github.com/AkifumiSato/elm-todo/blob/6638102abc354a77073bf4a8fb5ba9901567307c/src/Main.elm

まとめ

ざっくりポイント絞って説明してきましたがElmはともかく楽しいし、JSばっかいじってる私みたいな人間には非常に新鮮でJSで「こういうことできたらいいな」が結構増えたりもすると思います。

まだ追加しかできないTodoとしては致命的状態なので、完成したらまた記事を書きたいと思います。

拙い説明でしたが、今回はここまで。