Elm

ElmでTodoリストを作る

なんかElmという関数型のAltJSがよさげらしいのでちょろっと書いてみた。ウェブアプリというととりあえずTodoリスト作ってみるみたいなところもあり(?)やってみました。自分の認識があっているのか、記事にまとめて見てもらうことで確認したいという思いや記事にまとめることによって自分の知識を整理したいという思いで書きました。ツッコミあったらお気軽にどうぞ。

今回参考にしたのはこちら。

プログラミング言語Elmの薄い本 · GitBook
https://www.gitbook.com/book/giisyu/elm_usui_book/details

イントロダクション · Elm Tutorial
https://www.elm-tutorial.org/jp/

Elm Packages
http://package.elm-lang.org/

というわけで行ってみよう

Model

Modelで表示すべきデータを定義します。

import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick, onInput)

main =
  Html.beginnerProgram { model = model, view = view, update = update }

-- MODEL

type alias Model =
    { newTodo : String
    , todoList : List String
    }

model : Model
model = { newTodo = ""
        , todoList = []
        }

type aliasでModelをレコード型(Pythonでいうディクショナリみたいなもの)として定義しています。型は先頭が大文字でなければいけません。newTodoString型で入力欄に入っている文字列を、todoListStringList型で入力済みのTodoリストを表します。最初はそれぞれ空の文字列と空のリストをこのModelの更新の仕方と表示の仕方を定義していきます。

Update

Updateでは後述するViewから送られてきたメッセージによってモデルを更新します。

-- UPDATE

type Msg = Change String | Add | Delete Int

メッセージをユニオン型で定義します。ユニオン型は列挙型にほかのデータ型を合わせたような型で、識別子のような感じで使えます。例えばChange StringChange "hogehoge"という一つの識別子としてパターンマッチに使えます。ここではChange Stringはフォームに入力があったとき、AddはAdd Todoボタンが押されたとき、Delete Intは削除ボタンが押されたときにそれぞれ送られてくるメッセージとします。次に実際のモデルの更新について見ていきましょう。

update : Msg -> Model -> Model
update msg model =
    let
        isSpace = String.trim >> String.isEmpty
    in
        case msg of
            Change str ->
                { model | newTodo = str }
            Add ->
                if isSpace model.newTodo then
                    model
                else
                    { model | todoList = model.newTodo :: model.todoList
                    , newTodo = "" }
            Delete n ->
                let
                    t = model.todoList
                in          
                    { model |
                          todoList = List.take n t ++ List.drop (n + 1) t}

update関数はメッセージmsgとモデルmodelを受け取ってモデルを返します。この中でメッセージによってどモデルが変化するか定義しています。
let ... inの中ではこの関数の中でのみ通用する関数を定義しています。ここではisSpaceという空の文字列かスペースだけの文字列のとき真になる関数を定義しました。
String.trimは文字列の両端の空白を取り去った文字列を返す関数で、String.isEmptyは空の文字列かどうか判別する関数です。この2つの関数を演算子>>を使い合成しています。

case msg ofでパターンマッチングを行います。msgの中身によって返すモデルを変えます。

Change strが来た場合、モデルのnewTodostrに更新します。

Addが来た場合はnewTodoがスペース文字列のときはモデルの更新を行なわず、そうでないときはModelを更新します。{ hoge | fuga = nyaaan }とするとfugaだけをnyaaanに更新したレコードが得られ、record.barとするとbarに束縛されている値が得られます。ここでは演算子::を使い古いtodoListの先頭にnewTodoを連結したものに更新します。この時newTodoは空にします。

Delete nが来た場合、n番目の要素を除いたリストを返します。List.take n tはリストtからn個(番目でない)を取り出したリストを返し、List.drop (n+1) tはリストtから(n+1)個を取り除いたリストを返します。これらのリストを演算子++を使って連結します。

View

最後にViewを見てみましょう。

-- VIEW

view : Model -> Html Msg
view model =
    div []
        [ input [ type_ "text"
                , placeholder "input your todo"
                , onInput Change
                , value model.newTodo] []
        , button [ onClick Add ] [ text "add todo" ]
        , div [] (showList model.todoList)
         ]

showList : List String -> List (Html Msg)
showList =
    let
        todos = List.indexedMap (,)
        column (n,s) = div []
                   [ text s
                   , button[ onClick (Delete n) ] [ text "×" ]
                   ]
    in
        todos >> List.map column  

HTMLでおなじみの名前が出てきました。これらはHtmlモジュールの関数になります。本来ならHtml.divのように呼び出さなければいけませんが、最初の方で

import Html exposing (..)

と宣言しておいたおかげでモジュール名を明示しなくとも呼び出せるようになります。HTMLの生成は、例えば

<div class="hoge">
    <span style="color:red">
        にゃーん
    </span>
    <span>
        トゥータ!(勢い良くスパークリングワインを開栓)
    </span>
</div>

というHTMLが欲しい場合、

foo = div [ class "hoge" ]
    [ span [ style [ ("color", "red") ] ] [ text "にゃーん"]
    , span [] [ text "トゥータ!(勢い良くスパークリングワインを開栓)" ]
    ]

みたいな感じで記述すればいいです(classstyleHtml.Attributesモジュールの関数です)。

大事な部分がUpdateにメッセージを送る部分です。まずは

        [ input [ type_ "text"
                , placeholder "input your todo"
                , onInput Change
                , value model.newTodo] []

ここではテキストフォームに入力があった場合、関数onInput(モジュールHtml.Eventsの関数)に渡している値がUpdateに送られます。ここではChange "入力された文字列"を送っています。onInputChange Stringを作るChangeを受け取ることによってメッセージがいい感じに作られて渡されます。

        , button [ onClick Add ] [ text "add todo" ]

この部分ではボタンが押された時にAddメッセージを送っています。

                   [ text s
                   , button[ onClick (Delete n) ] [ text "×" ]
                   ]

ここでは各Todoに対してDelete nメッセージを送るボタンを作っています。ここのn
List.indexedMapという関数を使って作っています。この関数の働きは公式の例をご覧いただくのが良いでしょう。

indexedMap (,) ["Tom","Sue","Bob"] == [ (0,"Tom"), (1,"Sue"), (2,"Bob") ]

(,)と言うのは少しわかりにくいですがタプルを作る関数です。この関数とリストを渡すと数字のリストと組み合わせてタプルを作ってくれます。他の言語で言うzipの特別な場合、という感じですね。

動作例

実際に動く例がこちらになります。

アーキテクチャ

今回使ったModel - Update - Viewの流れはElm初期のもので、初心者向けのものらしいです。実際にはCmd/Subというmodel、update、view、cmd、subscriptionsに分けたアーキテクチャを使うようです。このあたりは今後試していきたいところです。

感想

関数型言語でJavaScriptが書けるっていうのが楽しかったですね。それにHaskellを多少かじっていたとは言え3時間程度でここまで書けるのも楽しさにつながっていると感じます。もっと色々とElmでアプリケーションを書いて共有できればなと思います。

あ、あと今回のソースGithubに上げてみました。