31

I'm writing a web game in Elm with lot of time-dependent events and I'm looking for a way to schedule an event at a specific time delay.

In JavaScript I used setTimeout(f, timeout), which obviously worked very well, but - for various reasons - I want to avoid JavaScript code and use Elm alone.

I'm aware that I can subscribe to Tick at specific interval and recieve clock ticks, but this is not what I want - my delays have no reasonable common denominator (for example, two of the delays are 30ms and 500ms), and I want to avoid having to handle a lot of unnecessary ticks.

I also came across Task and Process - it seems that by using them I am somehow able to what I want with Task.perform failHandler successHandler (Process.sleep Time.second).

This works, but is not very intuitive - my handlers simply ignore all possible input and send same message. Moreover, I do not expect the timeout to ever fail, so creating the failure handler feels like feeding the library, which is not what I'd expect from such an elegant language.

Is there something like Task.delayMessage time message which would do exactly what I need to (send me a copy of its message argument after specified time), or do I have to make my own wrapper for it?

Tomasz Lewowski
  • 1,935
  • 21
  • 29
  • FYI, it seems that elm-lang/core is a little different regarding `Task.perform`. see http://package.elm-lang.org/packages/elm-lang/core/5.0.0 – Tosh Nov 15 '16 at 00:50
  • @Tosh indeed, it seems that it just got updated to support the no-failure case – Tomasz Lewowski Nov 15 '16 at 18:04

4 Answers4

38

An updated and simplified version of @wintvelt's answer for Elm v0.18 is:

delay : Time.Time -> msg -> Cmd msg
delay time msg =
  Process.sleep time
  |> Task.perform (\_ -> msg)

with the same usage

TankorSmash
  • 12,186
  • 6
  • 68
  • 106
31

One thing that may not be obvious at first is the fact that subscriptions can change based on the model. They are effectively evaluated after every update. You can use this fact, coupled with some fields in your model, to control what subscriptions are active at any time.

Here is an example that allows for a variable cursor blink interval:

subscriptions : Model -> Sub Msg
subscriptions model =
    if model.showCursor
        then Time.every model.cursorBlinkInterval (always ToggleCursor)
        else Sub.none

If I understand your concerns, this should overcome the potential for handling unnecessary ticks. You can have multiple subscriptions of different intervals by using Sub.batch.

Chad Gilbert
  • 36,115
  • 4
  • 89
  • 97
26

If you want something to happen "every x seconds", then a subscription like solution, as described by @ChadGilbert is what you need. (which is more or less like javascript's setInterval().

If, on the other hand you want something to happen only "once, after x seconds", then Process.sleep route is the way to go. This is the equivalent of javascript's setTimeOut(): after some time has passed, it does something once.

You probably have to make your own wrapper for it. Something like

-- for Elm 0.18
delay : Time -> msg -> Cmd msg
delay time msg =
  Process.sleep time
  |> Task.andThen (always <| Task.succeed msg)
  |> Task.perform identity

To use e.g. like this:

---
update msg model =
  case msg of
    NewStuff somethingNew ->
      ...

    Defer somethingNew ->
      model
      ! [ delay (Time.second * 5) <| NewStuff somethingNew ]
wintvelt
  • 13,855
  • 3
  • 38
  • 43
  • 6
    Is there a way to cancel the deferred action before the timeout expires? – Pablo Enrici May 17 '17 at 05:56
  • 4
    One way to "cancel" is to set a flag in the model and then check that flag in the case of the final Msg (`NewStuff` in the case above) – Grav Jul 20 '17 at 14:03
  • 1
    Edit: less stateful is to pass the model to `NewStuff` and then check against current model and cancel if they differ – Grav Jul 20 '17 at 14:38
  • Instead of canceling the message, I would agree to handle it differently. And to build a check inside the `SomethingNew` branch whether producing a new model with or without message is still necessary. A flag in model is most logical. Your `update` function already has access to the model, no need to pass it in the message. Rationale: Strictly speaking, your Elm code has no control over the stream of messages to your `update` function. So your `update` function should assume that messages can come in at any time and in any order. – wintvelt Jul 20 '17 at 14:56
  • @Grav Could you point me towards some documentation or sample code which does what you're describing? Thanks! – pdoherty926 Aug 07 '17 at 17:11
  • @wintvelt I'm passing it because I want to be able to compare it with the current, possibly newer model. If it has changed, I just don't do any actions. – Grav Aug 09 '17 at 13:12
  • @grav I see. In my own code, the `model` is usually quite big/ has lots of data (like user info, lists and stuff). In my messages, I only include the minimum necessary new data (like the ID of some record the user wants shown). If you want to check new vs old for everything in the model, then passing the entire model in the message is fine. E.g. when the model is small and only has small bits of info. But if you only check some data from the model, then I would advise not to pass the entire model. – wintvelt Aug 09 '17 at 14:10
  • @wintvelt Yes, to clarify, I'd only supply the relevant part of the model. Eg, in my case the it's for searching, so I only supply and compare the part of the model that has the search parameters. But my point is that using a flag is redundant in my case, since what I actually want to do is to compare some data. – Grav Aug 10 '17 at 17:33
5

Elm v0.19

To execute once and delay:

delay : Float -> msg -> Cmd msg
delay time msg =
    -- create a task that sleeps for `time`
    Process.sleep time
        |> -- once the sleep is over, ignore its output (using `always`)
           -- and then we create a new task that simply returns a success, and the msg
           Task.andThen (always <| Task.succeed msg)
        |> -- finally, we ask Elm to perform the Task, which
           -- takes the result of the above task and
           -- returns it to our update function
           Task.perform identity

To execute a repeating task:

every : Float -> (Posix -> msg) -> Sub msg
TankorSmash
  • 12,186
  • 6
  • 68
  • 106
iElectric
  • 5,633
  • 1
  • 29
  • 31