3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-10-10

前回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 bMaybe a 型の値が Just a のときだけ a -> b の関数を適用して Just b の値を得ます。 Nothing であれば返り値もそのまま Nothing です。

次に、 Maybe.withDefault : a -> Maybe a -> aMaybe 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 を変えてみましょう。

次回 : Elm 0.18 入門(5) Subscriptionsによるイベント処理、関数合成

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?