如何在 Elm 0.17/0.18 中获取当前时间?

14
我已经问过这个问题了:
如何在 Elm 中获取当前时间?

我通过编写自己的 (现在已不建议使用) start-app 变体进行了回答:
http://package.elm-lang.org/packages/z5h/time-app/1.0.1

当然,Elm 架构已经发生了变化,我的旧方法不再奏效,因为没有信号或 Time.timestamp
所以....
假设我使用标准的 update 函数签名建立一个应用程序:
update : Msg -> Model -> (Model, Cmd Msg)

我想在更新时给我的模型加上时间戳。一个不可接受的近似解决方案是订阅 Time.every。从概念上讲,这不是我想要的。这是用时间更新模型并单独用消息更新模型。
我想要的是能够编写一个带有签名的更新函数:
updateWithTime: Msg -> Time -> Model -> (Model,Cmd Msg)
我开始尝试通过添加一些额外的消息来解决这个问题:
Msg = ... When | NewTime Time

并创建一个新的命令:
timeCmd = perform (\x -> NewTime 0.0) NewTime Time.now

所以在任何操作中,我都可以触发额外的命令来检索时间。但这很快变得混乱而失控。
有任何想法可以使代码更加清晰吗?
6个回答

9

我发现了一个比已接受的答案更优雅的解决方案。不再使用两个单独的模型,而是GetTimeAndThen消息保存一个返回消息的处理程序。这段代码感觉更自然、更像elm,并且可以以更通用的方式使用:

module Main exposing (..)

import Html exposing (div, button, text)
import Html.App as App
import Html.Events exposing (onClick)
import Task
import Time exposing (Time)


main =
    App.program
        { init = ( Model 0 0, Cmd.none )
        , view = view
        , update = update
        , subscriptions = (\_ -> Sub.none)
        }


view model =
    div []
        [ button [ onClick decrement ] [ text "-" ]
        , div [] [ text (toString model) ]
        , button [ onClick increment ] [ text "+" ]
        ]


increment =
    GetTimeAndThen (\time -> Increment time)


decrement =
    GetTimeAndThen (\time -> Decrement time)


type Msg
    = Increment Time
    | Decrement Time
    | GetTimeAndThen (Time -> Msg)


type alias Model =
    { count : Int, updateTime : Time }


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        GetTimeAndThen successHandler ->
            ( model, (Task.perform assertNeverHandler successHandler Time.now) )

        Increment time ->
            ( { model | count = model.count + 1, updateTime = time }, Cmd.none )

        Decrement time ->
            ( { model | count = model.count - 1, updateTime = time }, Cmd.none )


assertNeverHandler : a -> b
assertNeverHandler =
    (\_ -> Debug.crash "This should never happen")

1
我刚刚重构了一些代码,使用了这种风格,非常喜欢。将其设置为接受的答案。 - Mark Bolusmjak
2
虽然我真的很喜欢这个答案,但我会取消将其选为正确答案。原因是,如果应用程序的模型和消息可序列化,则可以记录和回放应用程序的历史记录(对于时间旅行调试器非常重要)。函数不可序列化,因此在模型或操作中包含函数无法使用时间旅行调试器。 - Mark Bolusmjak
4
这是一个有趣的想法,我从未考虑过。我应该指出,目前 Elm 中没有时间旅行调试器 (https://github.com/elm-lang/elm-reactor),不过它最终会回来的。 - w.brian
是的,另一个原因是Elm的维护者Evan Czaplicki说过:“The Elm Architecture的核心规则之一是不要将函数放在您的Model或Msg类型中。”我猜他这么说是有预见性的,因为他对Elm的计划有所了解。请参阅此处:http://package.elm-lang.org/packages/evancz/elm-sortable-table/latest - Mark Bolusmjak

9

一种方法是在不需要每次更新路径时获取时间的情况下,将您的 Msg 包装在另一种消息类型中,该类型将获取时间,然后使用时间调用您的常规 update。这是http://elm-lang.org/examples/buttons的修改版本,它将在每次更新时在模型上更新时间戳。

import Html exposing (div, button, text)
import Html.App exposing (program)
import Html.Events exposing (onClick)
import Task
import Time exposing (Time)


main =
  program { init = (Model 0 0, Cmd.none), view = view, update = update, subscriptions = (\_ -> Sub.none) }

type alias Model =
  { count: Int
  , updateTime : Time
  }

view model =
  Html.App.map GetTimeAndThen (modelView model)

type Msg
  = GetTimeAndThen ModelMsg
  | GotTime ModelMsg Time

update msg model =
  case msg of
    GetTimeAndThen wrappedMsg ->
      (model, Task.perform (\_ -> Debug.crash "") (GotTime wrappedMsg) Time.now)

    GotTime wrappedMsg time ->
      let
        (newModel, cmd) = modelUpdate wrappedMsg time model
      in
        (newModel, Cmd.map GetTimeAndThen cmd)

type ModelMsg = Increment | Decrement

modelUpdate msg time model =
  case msg of
    Increment ->
      ({model | count = model.count + 1, updateTime = time}, Cmd.none)

    Decrement ->
      ({model | count = model.count - 1, updateTime = time}, Cmd.none)

modelView model =
  div []
    [ button [ onClick  Decrement ] [ text "-" ]
    , div [] [ text (toString model.count) ]
    , button [ onClick  Increment ] [ text "+" ]
    , div [] [ text (toString model.updateTime) ]
    ]

干得好!我已经将那个示例适应到了 Elm 0.18 的工作环境中,这是一个 Gist:https://gist.github.com/r-k-b/e589b02d68cab07af63347507c8d0a2d - Robert K. Bell

7

elm-0.18完整实例 https://runelm.io/c/72i

import Time exposing (Time)
import Html exposing (..)
import Html.Events exposing (onClick)
import Task

type Msg
    = GetTime
    | NewTime Time

type alias Model =
    { currentTime : Maybe Time
    }

view : Model -> Html Msg
view model =
    let
        currentTime =
            case model.currentTime of
                Nothing ->
                    text ""

                Just theTime ->
                    text <| toString theTime
    in
        div []
            [ button [ onClick GetTime ] [ text "get time" ]
            , currentTime
            ]

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        GetTime ->
            model ! [ Task.perform NewTime Time.now ]

        NewTime time ->
            { model | currentTime = Just time } ! []

main : Program Never Model Msg
main =
    program
        { init = init
        , update = update
        , view = view
        , subscriptions = always Sub.none
        }

init : ( Model, Cmd Msg )
init =
    { currentTime = Nothing } ! []

3

在 Slack 上讨论了这个问题后,这里提供一种没有在 Msg 中使用函数的替代实现。与被接受的答案一样,只有当 Time.now Task 成功时才会更新模型。

import Html exposing (div, button, text)
import Html.App as App
import Html.Events exposing (onClick)
import Task
import Time exposing (Time)


main =
    App.program
        { init = init
        , view = view
        , update = update
        , subscriptions = (\_ -> Sub.none)
        }


view model =
    div []
        [ button [ onClick Decrement ] [ text "-" ]
        , div [] [ text (toString model) ]
        , button [ onClick Increment ] [ text "+" ]
        ]


type Msg
    = NoOp
    | Increment 
    | Decrement
    | GetTimeSuccess Msg Time
    | GetTimeFailure String


type alias Model =
    { count : Int, updateTime : Result String Time }

init : (Model , Cmd Msg)
init = 
  ( { count = 0
    , updateTime = Err "No time yet!"
    }
  , Task.perform  GetTimeFailure  (GetTimeSuccess NoOp) Time.now
  )


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        NoOp -> (model, Cmd.none)

        Increment ->
            ( model
            , Task.perform  GetTimeFailure  (GetTimeSuccess Increment) Time.now
            )

        Decrement ->
            ( model
            , Task.perform  GetTimeFailure (GetTimeSuccess Decrement) Time.now
            )


        GetTimeSuccess Increment time ->
            ( { model | count = model.count + 1, updateTime = Ok time}
            , Cmd.none
            )

        GetTimeSuccess Decrement time ->
            ( { model | count = model.count - 1, updateTime = Ok time}
            , Cmd.none
            )            

        GetTimeSuccess _ time ->
            ( { model |  updateTime = Ok time}
            , Cmd.none
            )

        GetTimeFailure msg ->
            ( { model | updateTime = Err msg}
            , Cmd.none
            )

这与@rofrol的答案有何不同? - lifebalance

2

我有一个关于自己问题的答案(基于amilner42的建议)。我正在我的当前代码中使用这个解决方案。

我非常喜欢@w.brian的解决方案,但是消息中的函数会破坏调试器。
我喜欢@robertjlooby的解决方案,这很相似,虽然它不需要额外的类型,并且已经更新到0.18版本。

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        NoOp ->
            model ! []

        TickThen msg ->
            model ! [ Task.perform (Tock msg) Time.now ]

        Tock msg time ->
                updateTimeStampedModel msg { model | time = time }

        otherMsg ->
            update (TickThen msg) model


updateTimeStampedModel : Msg -> Model -> ( Model, Cmd Msg )
updateTimeStampedModel msg model =
    case msg of
        NoOp ->
            update msg model

        TickThen _ ->
            update msg model

        Tock _ _ ->
            update msg model

        -- ALL OTHER MESSAGES ARE HANDLED HERE, AND ARE CODED TO ASSUME model.time IS UP-TO-DATE.

1
你可以创建一个本地模块,然后公开一个timestamp函数,该函数从JavaScript中的Date.now()获取时间。
大致上看起来是这样的:

Timestamp.elm

module Timestamp exposing (timestamp)

import Native.Timestamp

timestamp : () -> Int
timestamp a = Native.Timestamp.timestamp a

Native/Timestamp.js

var _YourRepoUserName$your_repo$Native_Timestamp = function() {
  return { timestamp: function(a) {return Date.now()}
}

Main.elm

port module Main exposing (..)

import Timestamp exposing (timestamp)

然后你可以在 Elm 中的任何地方使用 (timestamp()) 来获取当前时间戳作为 Int。


注意:我使用了timestamp : () -> Int,因为否则我无法使其正常工作。timestamp : Int只会返回第一次加载的硬编码时间。如果有改进,请告诉我。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接