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.PosixTime.Zone

プログラミングにおいて時間を正しく取り扱うためには、以下の3つの概念が必要となります:

  • 人間にとっての時間 — これは時計(午前8時)やカレンダー(5月3日)で目にするものです。しかし、もしボストンで午前8時に電話をかけると、バンクーバーにいる友人にとっては何時になるでしょうか?もし、東京で午前8時だとして、ニューヨークでも同じ日付なのでしょうか?(違います!)人間にとっての時間とは、常に変化する政治的な境界に基づいたタイムゾーンや、一貫性のない使われ方をするサマータイムによって異なってしまうので、基本的にはModelやデータベースに保存されるべきではありません。あくまでも表示だけの目的とすべきでしょう!
  • POSIX時間 — POSIX時間では、どこに住んでいるとか、どの時期であるとかは関係ありません。あくまでも(1970年の)ある時間からの経過秒数を数字として表しているだけです。地球上のどこに行こうとも、POSIX時間は同じなのです。
  • タイムゾーン — "タイムゾーン"とは、POSIX時間から人間にとっての時間へ変換するために必要なデータの集まりです。これは単なるUTC-7とかUTC+3などとは 異なります !タイムゾーンは単純なオフセットではなく、かなりもっと複雑なのです!フロリダにおけるサマータイムへの恒久的な移行サモアでのUTC-11からUCT+13への移行、などとIANAのタイムゾーンデータベースに無慈悲な脚注が追加されてしまいます。このデータベースが個々のコンピュータに読み込まれ、POSIX時間をデータベースに記載された全てのコーナーケースの情報を反映するよう計算することにより、人間にとっての時間として表示することが可能となるのです。

したがって人に対して時間を示すためには、常にTime.PosixTime.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.ZoneTime.Posixを引数にとり、あなたのタイムゾーンにおける時間を示す0から23Int型の数字を返します。

時間を取り扱うためのさらに多くの情報が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つの引数を取ります:

  1. ミリ秒単位での時間間隔。1000と言えば毎秒を意味します。60 * 1000とすれば毎分、5 * 60 * 1000とすれば5分毎と言う事も出来ます。
  2. 現時刻をあるMsgに変換する関数、すなわち毎秒ごとに、現時刻がupdate関数のためのTick <time>メッセージに変換されていきます。

これがサブスクリプションの基本的なパターンです。何らかの設定を与え、どのようにMsgの値を発行するか記述します。悪くないでしょ!

Task.perform

Time.Zoneを取得するのはちょっと工夫が必要です。次のようなコマンドを発行しました:

Task.perform AdjustTimeZone Time.here

この行を理解するための最良の方法は、Taskのドキュメントに一通り目を通すことです。そのドキュメントの中で、実際にTaskという新しい概念について説明がなされています。ここで中途半端な説明をして脱線するのはやめようと思いますが、大事な点はランタイムシステムに対して、このコードの実行時にTime.Zoneを返すように指示しているだけということです。

練習問題:

  • 時計を止めるためのボタンを追加し、Time.everyのサブスクリプションを止めてみましょう。
  • デジタル時計の見栄えを良くしてください。なんらかのstyle属性を追加してみましょう。
  • elm/svgを使って、赤い秒針のアナログ時計を作ってみましょう。

results matching ""

    No results matching ""