前回は Html.program
を利用してアクションの連鎖の仕方を確認しました。
今回はまず前回までのコードを少しリファクタリングして、時刻に関する処理を見てみましょう。
リファクタリング
アクションの連鎖そのものは前回のようにすれば可能なのですが、よく考えてみると2つのアクションは連動していて切り離して考える必要はありません。
ですから、一つのアクションにまとめましょう。
まず、入力された文字を数値判定してモデルの状態を一括で更新する関数を作ります。
updateCountStep : String -> Model -> Model
updateCountStep s model =
case (toInt s) of
Ok num ->
{ model | countStepInput = s, countStepNum = num }
Err _ ->
{ model | countStepInput = s }
これとよく似た関数を既に以下のように作っていました。
convertInputToMsg : String -> Msg
convertInputToMsg s =
case (toInt s) of
Ok num ->
UpdateCountStepNum num
Err msg ->
NoOp
UpdateCountStepNum
アクションで行っていた内容を統合しましたので、この関数は不要となります。
加えて、 UpdateCountStepNum
アクション自体も不要となりますので削除しましょう。
type Msg
= NoOp
| Increase Int
| UpdateCountStepInput String
- | UpdateCountStepNum Int
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
NoOp ->
model ! []
Increase num ->
{ model | count = model.count + num } ! []
UpdateCountStepInput s ->
- { model | countStepInput = s } ! [ Task.perform convertInputToMsg (Task.succeed s) ]
+ updateCountStep s model ! []
- UpdateCountStepNum num ->
- { model | countStepNum = num } ! []
リファクタリングはこれで完了です。「値の入力」からの「モデルの更新」を具体的にスッキリ書き直しました。
ココまでのコード -> https://ellie-app.com/4fG9MSvW7Xha1/1
時刻の取得と表示
さて、時刻の話題に移りましょう。
一般に純粋関数型言語において「現在の時刻」というのは外的要因にアクセスすることになるため、「現在の時刻を返す」シンプルな関数は作れません(ある絶対指定の時刻(ex. 2017-09-08 19:13:00.000)を生成するだけなら通常の関数です)。
このような外的な情報にアクセスするとき、Elmではかならず Task
として実行します。例えば現在時刻を取得する関数 Date.now
のシグネチャは次のようになります。
now : Task x Date
すぐさま Date
型の値が返却されることはなく、 Task x Date
型の値になっています。(ここで x
が掛け算の記号に見える人もいると思いますが、型変数です)
前回、初期化時に値をセットするアクションを書いていましたので、その部分を更新しましょう。
まずはモデルに時刻を保存できるようにします。
type alias Model =
{ count : Int
, countStepInput : String
, countStepNum : Int
+ , datetime : Maybe Date
}
initModel : ( Model, Cmd Msg )
initModel =
{ count = 0
, countStepInput = ""
, countStepNum = 0
+ , datetime = Nothing
}
! [ Task.perform UpdateCountStepInput (Task.succeed "5") ]
日時は Date
型の値なのですが、 Maybe Date
型として定義しました。これは、先程述べたように現在時刻を取得する関数はないので、初期化実行後に update
を通じて更新する必要があり初期化時点では「値が存在しない」ことを明示する必要があるためです。
type Maybe a = Just a | Nothing
は値のないケースを Nothing
, 値があるケースを Just a
として表現できる型です。
Date
型を利用するので、 import を追加しましょう。
import Date exposing (Date)
続けて、更新するためのアクションを定義します。
type Msg
= NoOp
| Increase Int
| UpdateCountStepInput String
+ | UpdateDatetime Date
こちらは日時が取得できた時に送られるアクションのため、 Maybe Date
ではなく Date
型の値を扱います。
このアクションに対する具体的な処理は次のようになります。
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
NoOp ->
model ! []
Increase num ->
{ model | count = model.count + num } ! []
UpdateCountStepInput s ->
updateCountStep s model ! []
+ UpdateDatetime dt ->
+ { model | datetime = Just dt } ! []
値が存在することを Just dt
で示しています。
続いて、日時を取得するタスクを実行する関数を作ります。
getCurrentDate : Cmd Msg
getCurrentDate =
let
handleResult result =
case result of
Ok dt ->
UpdateDatetime dt
Err _ ->
NoOp
in
Task.attempt handleResult Date.now
ちょっと複雑ですね。まず Task.attempt : (Result x a -> msg) -> Task x a -> Cmg msg
は、 Result x a -> msg
という関数を使ってエラーの場合を含めたアクションへの変換を行う関数を使って、 Task x a
を実行します。
ここでは handleResult
がアクションへの変換で、 Date.now
がタスクです。
let
に変数や関数を一時的に定義して、 in
で利用することができます。 in
では書きたい処理をスッキリ書いて、細かい内容は let
に定義しておくと見通しが良くなりますし、匿名関数で書くより名前付きの関数のほうが間違いにくくなります。
最後に、このタスク実行関数を初期化時に呼び出しましょう。
initModel : ( Model, Cmd Msg )
initModel =
{ count = 0
, countStepInput = ""
, countStepNum = 0
, datetime = Nothing
}
- ! [ Task.perform UpdateCountStepInput (Task.succeed "5") ]
+ ! [ getCurrentDate ]
再コンパイルして初期化時に呼び出されているか確認しましょう。
おっと、Viewを何も変更していないので、時刻が取得できているかわかりませんね。
次のようにViewを更新してみましょう。
view : Model -> Html Msg
view model =
div []
[ counter model
, increaseButton model
, stepInput model
- , text ("countStepInput = " ++ model.countStepInput)
+ , text (Maybe.withDefault "No datetime." (Maybe.map toString model.datetime))
]
長くなってきましたね。処理順に右から説明しましょう。
Maybe.map : (a -> b) -> Maybe a -> Maybe b
は Maybe a
型の値が Just a
のときだけ a -> b
の関数を適用して Just b
の値を得ます。 Nothing
であれば返り値もそのまま Nothing
です。
次に、 Maybe.withDefault : a -> Maybe a -> a
は Maybe a
型の値が Just a
であればその値を取り出して返し、 Nothing
のときは代わりに第一引数の値を返します。つまり、値が存在しない場合のデフォルト値です。
この2つによって、 Maybe Date => Maybe String => String
という変換を行って、 text
関数に渡しています。
さて、画面上には日時が表示されたでしょうか?
ココまでのコード -> https://ellie-app.com/cHPGxstX9a1/0
括弧を使わないでスッキリ書く
最後に書いた Maybe
のくだりを括弧を使わずに、しかも処理順が分かりやすいように書き直してみましょう。
view : Model -> Html Msg
view model =
div []
[ counter model
, increaseButton model
, stepInput model
- , text (Maybe.withDefault "No datetime." (Maybe.map toString model.datetime))
+ , model.datetime
+ |> Maybe.map toString
+ |> Maybe.withDefault "No datetime."
+ |> text
]
|>
という見慣れない記号が出てきました。これも関数で、 (|>) : a -> (a -> b) -> b
というシグネチャを持っています。
中置演算子(実際には関数)と呼ばれるもので、名前の通り中間に置くことができます。つまり、実際には
model.datetime |> (Maybe.map toString)
が
(|>) model.datetime (Maybe.map toString)
のように適用されており、結果として Maybe.map toString model.datetime
に等しくなります。
これを組み合わせることで、 model.datetime
を、 toString
で文字列化して、値がない場合は "No datetime."
で置き換えて、 HTMLテキストとして出力する、という順番が明確になります。
ココまでのコード -> https://ellie-app.com/cHPGxstX9a1/1
まとめ
現在時刻の取得方法やそれに伴う Maybe
の使い方などを見てきました。
次は放置状態の subscriptions
を変えてみましょう。