はじめに
初心者Elm入門その3で紹介したElmアーキテクチャを使ってもう少し複雑な例に取り組んでみよう。
削除機能付きToDoリスト
完成画面はこのようになる。
入力フィールドに文字を入力すると「Add ToDo」ボタンが押せるようになり、ToDoリスト中の文字をクリックすることで削除できる。
さっそくコードを見よう。
まずは自力バージョン。
のちに改良バージョンも示す。
module Main exposing (main)
import Browser
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
main : Program () Model Msg
main =
Browser.sandbox
{ init = init
, update = update
, view = view
}
-- MODEL
type alias Model = -- 今回のModelはレコード。
{ input : String -- 入力フィールド中の文字列を保持する。
, memos : List String -- 文字列を要素とするリスト。
} -- 上記の例では memos = ["モンドール", "ワイン", "醤油", "卵", "食パン"]
-- のようになる。submitされた文字列を格納する。
init : Model -- input初期値は""、memos初期値は[]
init =
{ input = ""
, memos = []
}
-- UPDATE
type Msg
= Input String
| Submit
| Done Int
update : Msg -> Model -> Model
update msg model =
case msg of
Input input -> -- 入力フィールドの文字が変わるたびに
{ model | input = input } -- inputとして受け取る。
Submit -> -- 「Add ToDo」ボタンが押されると
{ model
| input = "" -- 入力フィールドを空にし
, memos = model.input :: model.memos -- memosの先頭にinput文字列を加える。
}
Done done -> -- done(Int型)番目の要素がクリックされると
{ model
| memos = List.take done model.memos ++ List.drop (done + 1) model.memos
} -- 0~(done-1)番目までのmemosと
-- (done+1)~最後までのmemosが連結されたリストを
-- 新しいmemosとする。
-- VIEW
view : Model -> Html Msg
view model =
div []
[ Html.form [ onSubmit Submit ]
[ input [ value model.input, onInput Input ] []
, button
[ disabled (String.length model.input < 1) ] -- inputのlengthが1未満だとボタンが押せない。
[ text "Add ToDo" ]
]
, ul [] (viewMemo model.memos) -- viewMemo関数の引数としてmemos(List String)をとる。
]
htmlFunc : ( Int, String ) -> Html Msg -- ややこしいので地の文で説明します。
htmlFunc tuple =
let
i =
Tuple.first tuple
memo =
Tuple.second tuple
in
li [ onClick (Done i) ] [ text memo ]
viewMemo : List String -> List (Html Msg)
viewMemo memos =
let
memoIndexPair =
List.indexedMap Tuple.pair
in
List.map htmlFunc (memoIndexPair memos) -- (*)
(*) memoIndexPair関数はList Stringを渡すと[(0, "文字列0"), (1, "文字列1"), ...]
というタプルを要素とするリストを返してくれる。
例ではmemos = ["モンドール", "ワイン", "醤油", "卵", "食パン"]
なので、memoIndexPair memos
で[(0, "モンドール"), (1, "ワイン"), (2, "醤油"), (3, "卵"), (4, "食パン")]
となる。
これに対してList.mapで各々の要素についてhtmlFuncを適用する。
htmlFuncはタプルの0番目の要素をiとし、1番目の要素をmemoとしてHtml Msgであるli [ onClick (Done i) ] [ text memo ]
を作る。
これで完成!......なんだけど、会社の先輩が「もっと簡単に書けるよ!」って教えてくれたのでちょっと改良する。
削除機能付きToDoリスト(改)
以下改良バージョン。
変更したのはややこしかったhtmlFuncとviewMemoの部分のみ。
module Main exposing (main)
import Browser
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
main : Program () Model Msg
main =
Browser.sandbox
{ init = init
, update = update
, view = view
}
-- MODEL
type alias Model =
{ input : String
, memos : List String
}
init : Model
init =
{ input = ""
, memos = []
}
-- UPDATE
type Msg
= Input String
| Submit
| Done Int
update : Msg -> Model -> Model
update msg model =
case msg of
Input input ->
{ model | input = input }
Submit ->
{ model
| input = ""
, memos = model.input :: model.memos
}
Done done ->
{ model
| memos = List.take done model.memos ++ List.drop (done + 1) model.memos
}
-- VIEW
view : Model -> Html Msg
view model =
div []
[ Html.form [ onSubmit Submit ]
[ input [ value model.input, onInput Input ] []
, button
[ disabled (String.length model.input < 1) ]
[ text "Add ToDo" ]
]
, ul [] (viewMemo model.memos)
]
htmlFunc : Int -> String -> Html Msg
htmlFunc i memo =
li [ onClick (Done i) ] [ text memo ]
viewMemo : List String -> List (Html Msg)
viewMemo =
List.indexedMap htmlFunc
とてもすっきりした。
しかし初心者としてはなぜ動くのかが分からない(分からなかった)。
まずviewMemoに引数がないのが意味が分からない。
これは関数と関数を合わせて関数を作るというものらしい(部分適用)。
具体例を示す。
> (*)
<function> : number -> number -> number
> (*) 2 3
6 : number
> f = (*) 2
<function> : number -> number
> f 3
6 : number
(*)というのは第1引数と第2引数のかけ算をする関数だ。
f = (*) 2
とすると(*)の第1引数が2に固定された関数fを作ることができる。
よってf 3
は6
となる。
ここで
> f = (*) 2
<function> : number -> number
> List.map
<function> : (a -> b) -> List a -> List b
> List.map f [1, 2, 3, 4, 5]
[2,4,6,8,10] : List number
> g = List.map f
<function> : List number -> List number
> g [1, 2, 3, 4, 5]
[2,4,6,8,10] : List number
List.map 関数 リスト
はリストの要素ひとつひとつに関数を適用して展開するものである。
上記例では[1, 2, 3, 4, 5]
の各要素を2倍して展開する。
g = List.map f
と置くことでfしながらmapもしてくれる関数をgと呼ぶことにしたということ。
ToDoList.elmに戻ろう。
List.indexedMapの定義は(Basics.Int -> a -> b) -> List.List a -> List.List b
なので、この第1引数をInt -> String -> Html Msg
であるhtmlFuncに固定してやる。
そうすることで、List Stringが来たときにList (Html Msg)を返す関数viewMemoが出来る。
viewMemoはList.indexedMapとhtmlFuncの機能を同時に行ってくれる関数と見ることが出来るのでmemosの要素番号と要素が取得出来てli [ onClick (Done i) ] [ text memo ]
が得られる。
試し方
- 適当なディレクトリを作る。
-
elm init
する。 - srcというディレクトリができているのでsrc内にToDoList.elmを置く。
-
elm make src/ToDoList.elm --output=todolist.html
する。(--outputは必須ではないが指定しないとindex.htmlという名前でコンパイルされる。) - todolist.htmlが出来るのでブラウザで開く。