Time
まずはデジタル時計を作って見ましょう(アナログ時計は今後の課題です!)
これまでは、コマンドに注目してきました。乱数に関する例では、ランダムな値をよこすようランタイムシステムに対してコマンドを発行しましたが、時計の例の場合には奇妙な感じのパターンとなってしまいます。現在の時刻を 常に 知りたいのです。ここで サブスクリプション が登場します。
まず青い "Edit" ボタンをクリックしてオンラインエディターでコードに目を通すところから始めましょう。
import Browser
import Html exposing (..)
import Task
import Time
-- MAIN
main =
Browser.element
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
-- MODEL
type alias Model =
{ zone : Time.Zone
, time : Time.Posix
}
init : () -> (Model, Cmd Msg)
init _ =
( Model Time.utc (Time.millisToPosix 0)
, Task.perform AdjustTimeZone Time.here
)
-- UPDATE
type Msg
= Tick Time.Posix
| AdjustTimeZone Time.Zone
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
Tick newTime ->
( { model | time = newTime }
, Cmd.none
)
AdjustTimeZone newZone ->
( { model | zone = newZone }
, Cmd.none
)
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions model =
Time.every 1000 Tick
-- VIEW
view : Model -> Html Msg
view model =
let
hour = String.fromInt (Time.toHour model.zone model.time)
minute = String.fromInt (Time.toMinute model.zone model.time)
second = String.fromInt (Time.toSecond model.zone model.time)
in
h1 [] [ text (hour ++ ":" ++ minute ++ ":" ++ second) ]
新しく出てきたものはすべて elm/time
パッケージに由来しています。その部分を取り上げて見ていきましょう!
Time.Posix
と Time.Zone
プログラミングにおいて時間を正しく取り扱うためには、以下の3つの概念が必要となります:
- 人間にとっての時間 — これは時計(午前8時)やカレンダー(5月3日)で目にするものです。しかし、もしボストンで午前8時に電話をかけると、バンクーバーにいる友人にとっては何時になるでしょうか?もし、東京で午前8時だとして、ニューヨークでも同じ日付なのでしょうか?(違います!)人間にとっての時間とは、常に変化する政治的な境界に基づいたタイムゾーンや、一貫性のない使われ方をするサマータイムによって異なってしまうので、基本的には
Model
やデータベースに保存されるべきではありません。あくまでも表示だけの目的とすべきでしょう!
- POSIX時間 — POSIX時間では、どこに住んでいるとか、どの時期であるとかは関係ありません。あくまでも(1970年の)ある時間からの経過秒数を数字として表しているだけです。地球上のどこに行こうとも、POSIX時間は同じなのです。
- タイムゾーン — "タイムゾーン"とは、POSIX時間から人間にとっての時間へ変換するために必要なデータの集まりです。これは単なる
UTC-7
とかUTC+3
などとは 異なります !タイムゾーンは単純なオフセットではなく、かなりもっと複雑なのです!フロリダにおけるサマータイムへの恒久的な移行、サモアでのUTC-11からUCT+13への移行、などとIANAのタイムゾーンデータベースに無慈悲な脚注が追加されてしまいます。このデータベースが個々のコンピュータに読み込まれ、POSIX時間をデータベースに記載された全てのコーナーケースの情報を反映するよう計算することにより、人間にとっての時間として表示することが可能となるのです。
したがって人に対して時間を示すためには、常にTime.Posix
とTime.Zone
について知っていなければなりません。そうです!全ての"人間にとっての時間"なるものはview
関数のためであって、Model
のためではありません。その事は以下のview
に見ることができます:
view : Model -> Html Msg
view model =
let
hour = String.fromInt (Time.toHour model.zone model.time)
minute = String.fromInt (Time.toMinute model.zone model.time)
second = String.fromInt (Time.toSecond model.zone model.time)
in
h1 [] [ text (hour ++ ":" ++ minute ++ ":" ++ second) ]
関数Time.toHour
は、Time.Zone
とTime.Posix
を引数にとり、あなたのタイムゾーンにおける時間を示す0
から23
のInt
型の数字を返します。
時間を取り扱うためのさらに多くの情報がelm/time
のREADMEに含まれていますので、時間についてもっと何かする前には必ず読むべきです!特に、もしスケジューリングやカレンダー等について何かするなら読んでみてください。
subscriptions
それではどのようにTime.Posix
を取得するのでしょうか?そうサブスクリプションです!
subscriptions : Model -> Sub Msg
subscriptions model =
Time.every 1000 Tick
Time.every
関数を使っています:
every : Float -> (Time.Posix -> msg) -> Sub msg
この関数は2つの引数を取ります:
- ミリ秒単位での時間間隔。
1000
と言えば毎秒を意味します。60 * 1000
とすれば毎分、5 * 60 * 1000
とすれば5分毎と言う事も出来ます。 - 現時刻をある
Msg
に変換する関数、すなわち毎秒ごとに、現時刻がupdate
関数のためのTick <time>
メッセージに変換されていきます。
これがサブスクリプションの基本的なパターンです。何らかの設定を与え、どのようにMsg
の値を発行するか記述します。悪くないでしょ!
Task.perform
Time.Zone
を取得するのはちょっと工夫が必要です。次のようなコマンドを発行しました:
Task.perform AdjustTimeZone Time.here
この行を理解するための最良の方法は、Task
のドキュメントに一通り目を通すことです。そのドキュメントの中で、実際にTask
という新しい概念について説明がなされています。ここで中途半端な説明をして脱線するのはやめようと思いますが、大事な点はランタイムシステムに対して、このコードの実行時にTime.Zone
を返すように指示しているだけということです。
練習問題: