Elm 0.18 入門(4) 時刻の取得とMaybe

[前回](http://qiita.com/mather314/items/01acf5d72b36dc55bbe1)は `Html.program` を利用してアクションの連鎖の仕方を確認しました。
今回はまず前回までのコードを少しリファクタリングして、時刻に関する処理を見てみましょう。

https://ellie-app.com/4fG9MSvW7Xha1/0

## リファクタリング

アクションの連鎖そのものは前回のようにすれば可能なのですが、よく考えてみると2つのアクションは連動していて切り離して考える必要はありません。
ですから、一つのアクションにまとめましょう。

まず、入力された文字を数値判定してモデルの状態を一括で更新する関数を作ります。

```hs
updateCountStep : String -> Model -> Model
updateCountStep s model =
    case (toInt s) of
        Ok num ->
            { model | countStepInput = s, countStepNum = num }

        Err _ ->
            { model | countStepInput = s }
```

これとよく似た関数を既に以下のように作っていました。

```hs
convertInputToMsg : String -> Msg
convertInputToMsg s =
    case (toInt s) of
        Ok num ->
            UpdateCountStepNum num

        Err msg ->
            NoOp
```

`UpdateCountStepNum` アクションで行っていた内容を統合しましたので、この関数は不要となります。
加えて、 `UpdateCountStepNum` アクション自体も不要となりますので削除しましょう。

```diff
 type Msg
     = NoOp
     | Increase Int
     | UpdateCountStepInput String
-    | UpdateCountStepNum Int
```

```diff
 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` のシグネチャは次のようになります。

```hs
now : Task x Date
```

すぐさま `Date` 型の値が返却されることはなく、 `Task x Date` 型の値になっています。（ここで `x` が掛け算の記号に見える人もいると思いますが、型変数です）
前回、初期化時に値をセットするアクションを書いていましたので、その部分を更新しましょう。

まずはモデルに時刻を保存できるようにします。

```diff
 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 を追加しましょう。

```hs
import Date exposing (Date)
```

続けて、更新するためのアクションを定義します。

```diff
 type Msg
     = NoOp
     | Increase Int
     | UpdateCountStepInput String
+    | UpdateDatetime Date
```

こちらは日時が取得できた時に送られるアクションのため、 `Maybe Date` ではなく `Date` 型の値を扱います。
このアクションに対する具体的な処理は次のようになります。

```diff
 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` で示しています。

続いて、日時を取得するタスクを実行する関数を作ります。

```hs
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` に定義しておくと見通しが良くなりますし、匿名関数で書くより名前付きの関数のほうが間違いにくくなります。

最後に、このタスク実行関数を初期化時に呼び出しましょう。

```diff
 initModel : ( Model, Cmd Msg )
 initModel =
     { count = 0
     , countStepInput = ""
     , countStepNum = 0
     , datetime = Nothing
     }
-        ! [ Task.perform UpdateCountStepInput (Task.succeed "5") ]
+        ! [ getCurrentDate ]
```

再コンパイルして初期化時に呼び出されているか確認しましょう。
おっと、Viewを何も変更していないので、時刻が取得できているかわかりませんね。

次のようにViewを更新してみましょう。

```diff
 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` のときは代わりに第一引数の値を返します。つまり、値が存在しない場合のデフォルト値です。
この２つによって、 `Maybe Date => Maybe String => String` という変換を行って、 `text` 関数に渡しています。

さて、画面上には日時が表示されたでしょうか？

ココまでのコード -> https://ellie-app.com/cHPGxstX9a1/0

## 括弧を使わないでスッキリ書く

最後に書いた `Maybe` のくだりを括弧を使わずに、しかも処理順が分かりやすいように書き直してみましょう。

```diff
 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` というシグネチャを持っています。
中置演算子（実際には関数）と呼ばれるもので、名前の通り中間に置くことができます。つまり、実際には

```hs
model.datetime |> (Maybe.map toString)
```

が

```hs
(|>) model.datetime (Maybe.map toString)
```

のように適用されており、結果として `Maybe.map toString model.datetime` に等しくなります。
これを組み合わせることで、 `model.datetime` を、 `toString` で文字列化して、値がない場合は `"No datetime."` で置き換えて、 HTMLテキストとして出力する、という順番が明確になります。

ココまでのコード -> https://ellie-app.com/cHPGxstX9a1/1

## まとめ

現在時刻の取得方法やそれに伴う `Maybe` の使い方などを見てきました。
次は放置状態の `subscriptions` を変えてみましょう。

次回 : [Elm 0.18 入門(5) Subscriptionsによるイベント処理、関数合成](https://qiita.com/mather314/items/425af0aa6a13c39d41a4)