14

I had asked this question already:
How do I get the current time in Elm?

And answered it by writing my own (now deprecated) variant of start-app:
http://package.elm-lang.org/packages/z5h/time-app/1.0.1

Of course the Elm architecture has since changed, and my old way of doing things no longer works, because there are no signals or Time.timestamp.

So....

Suppose I build an app with the standard update function signature:
update : Msg -> Model -> (Model, Cmd Msg)

I'd like to timestamp my model with the time at update. One unacceptable almost-solution is to subscribe to Time.every. Conceptually this is not what I want. This is updating the model with time and also separately updating model with messages.

What I want is to be able to write an update function with signature:
updateWithTime : Msg -> Time -> Model -> (Model, Cmd Msg)


I started trying to solve this by adding some extra messages:
Msg = ... When | NewTime Time

And creating a new command:
timeCmd = perform (\x -> NewTime 0.0) NewTime Time.now

So in any action, I can fire off an extra command to retrieve the time. But this gets messy and out of hand quickly.

Any ideas on how I can clean this up?

Community
  • 1
  • 1
z5h
  • 21,947
  • 8
  • 64
  • 119

6 Answers6

9

One option without having to do the time fetch on every update path would be to wrap your Msg in another message type that would fetch the time and then call your normal update with the time. This is a modified version of http://elm-lang.org/examples/buttons that will update a timestamp on the model with every update.

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) ]
    ]
robertjlooby
  • 6,910
  • 2
  • 29
  • 45
  • Excellent work! I've adapted that example to work with Elm 0.18, here's a Gist: https://gist.github.com/r-k-b/e589b02d68cab07af63347507c8d0a2d – Robert K. Bell Feb 13 '17 at 11:18
8

I've found what I believe to be a more elegant solution than the accepted answer. Instead of having two separate models, the GetTimeAndThen message holds a handler that returns a message. The code feels much more natural and elm-like, and can be used in a more general fashion:

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")
w.brian
  • 13,695
  • 12
  • 57
  • 100
  • I just refactored some code to use this style and much prefer it. Setting as accepted answer. – z5h Aug 21 '16 at 15:45
  • 2
    Although I really like this answer, I'm going to un-select it as the correct answer. The reason is, if an app's model and messages are serializable, then an application's history can be recorded and played back (important for time travelling debugger). Functions are not serializable and so including a function in the model or an action cannot work with the time-traveling debugger. – z5h Sep 12 '16 at 01:09
  • 3
    That's an interesting thought, and something I never considered. I should note that [there currently is no time-traveling debugger in Elm](https://github.com/elm-lang/elm-reactor), although it will be coming back eventually. – w.brian Sep 12 '16 at 01:27
  • Ya, another reason is that Elm's maintainer Evan Czaplicki said "One of the core rules of The Elm Architecture is never put functions in your Model or Msg types.". I'm guessing he said that with foresight of his plans for Elm. See here: http://package.elm-lang.org/packages/evancz/elm-sortable-table/latest – z5h Sep 12 '16 at 01:54
7

elm-0.18 full example 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 } ! []
rofrol
  • 12,038
  • 7
  • 62
  • 63
3

Following a discussion about this question on Slack, here is an alternative implementation without functions in the Msg. As with the accepted answer, the model is only updated when the Time.now Task succeeds.

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
            )
Michael Thomas
  • 1,344
  • 7
  • 17
2

I have an answer to my own question (based on a suggestion by amilner42). I'm using this solution in my current code.

I very much like the solution by @w.brian, but functions in messages break the debugger.
I like the solution by @robertjlooby, and this is is very similar, though it does away with an extra type, and is updated for 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.
z5h
  • 21,947
  • 8
  • 64
  • 119
1

You could create a Native module, and then expose a timestamp function that gets the time from Date.now() in JavaScript.

This is roughly what it would look like:

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)

then you can use (timestamp ()) anywhere in Elm to get the current timestamp as an Int.


Note: I used timestamp : () -> Int because I couldn't get it to work otherwise. timestamp : Int simply returned the hardcoded time of first load.

Let me know if this could be improved.

AdrianoFerrari
  • 2,040
  • 18
  • 14