Delay introduces gliches

243 views
Skip to first unread message

Oleg Grenrus

unread,
Jul 27, 2015, 2:39:54 PM7/27/15
to elm-d...@googlegroups.com
The follow-up from github issue: https://github.com/elm-lang/elm-compiler/issues/1003

Short example:

import Graphics.Element exposing (..)
import Mouse
import Time

f : () -> () -> ()
f a b = ()

delayed : Signal ()
delayed = Time.delay 0 Mouse.clicks

foo : Signal ()
foo = Signal.map2 f delayed Mouse.clicks

bar : Signal Int
bar =
Signal.foldp (\click total -> total + 1) 0 foo

main : Signal Element
main =
Signal.map show bar


Click will increase counter by two.

So Time.delay 0 signal ≠ signal, is that documented somewhere?

Yet I'd prefer to be them to be equal. I'm not sure how, but it feels that order of definitions could be observable. That ruins refactorability (if that is a word).

Cheers,
Oleg Grenrus
signature.asc

Janis Voigtländer

unread,
Jul 27, 2015, 2:56:11 PM7/27/15
to elm-d...@googlegroups.com

Indeed, Time.delay 0 signal is not semantically equal to signal. It’s not documented anywhere I am aware of. And probably it will not be documented anywhere, because it might encourage people to use delay with 0, which is bad. I would actually make it a runtime error to call delay with 0 or a negative number (that runtime error could anyway only happen right at program start, so no surprises later on or after deployment). In practical terms, note that anything below 4 will anyway have indistinguishable effect. The implementation of delay uses JavaScript’s setTimeout, and if you google a bit about that method, you will find that even a setTimeout with argument 0 will actually be performed as a setTimeout with argument 4.



--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Dobes Vandermeer

unread,
Jul 27, 2015, 3:00:50 PM7/27/15
to elm-d...@googlegroups.com

It's not clear whether it is a glitch because the behavior of "delay" is not specified in this regard.  It doesn't really say whether or not two signals with the same delay should fire simultaneously, or if a delay of zero should be an identity.

It could be changed to specify that, though.  Intuitively it seems to make sense that if the same event is delayed twice with the same delay, that it would fire at the same time.  It's a weird kind of usage but seems valid.

A delay of zero, I think that might be an error.  Since delay takes a constant there's no reason to accept negative or zero delays.

Janis Voigtländer

unread,
Jul 27, 2015, 3:13:30 PM7/27/15
to elm-d...@googlegroups.com

About calling delay twice with the same number on the same signal: the resulting two signals will fire independently. The situation is like for Time.fps and Time.every. The first’s documentation has a sentence

Note: Calling fps 30 twice gives two independently running timers.

The second’s has a sentence

Note: Calling every 100 twice gives two independently running timers.

A similar sentence would be appropriate (and true) in the documentation of Time.delay.

Max Goldstein

unread,
Jul 27, 2015, 3:28:03 PM7/27/15
to Elm Discuss, janis.voi...@gmail.com
I never about using Time.delay with a negative number, and would support a runtime error for it since it can only happen immediately. Same would go for fps and every. This is an alternative to a silent error.

However delay 0 can be useful because the delayed signal will always fire after the parent signal. This is useful for turning a steady-state value into a pulse.

Max Goldstein

unread,
Jul 27, 2015, 3:28:42 PM7/27/15
to Elm Discuss, janis.voi...@gmail.com, maxgol...@gmail.com
Correction: never thought about. Sorry.

Jeff Smits

unread,
Jul 27, 2015, 3:51:37 PM7/27/15
to elm-discuss, Janis Voigtländer, Max Goldstein
I guess this problem has to do with asynchronous effects on events and equality of signals. Seems like a hard, theoretical problem to keep referential transparency there.

On Mon, Jul 27, 2015 at 12:28 PM, Max Goldstein <maxgol...@gmail.com> wrote:
Correction: never thought about. Sorry.

Janis Voigtländer

unread,
Jul 27, 2015, 3:53:50 PM7/27/15
to Elm Discuss
Arguably, it would be more honest to do that with delay 1, i.e., using the least positive number, instead of 0, which gives the wrong impression that there is no delay.

Max Goldstein

unread,
Jul 27, 2015, 5:44:45 PM7/27/15
to Elm Discuss, janis.voi...@gmail.com
But that's exactly the point - there is the smallest amount of delay the runtime can provide. A hypothetical but plausible runtime might be able to provide sub-millisecond delay. Granted, this relies on knowing about setTimeout and the single-threaded JS environment, and saying essentially, "do this as soon as you have a free moment".

Jeff Smits

unread,
Jul 27, 2015, 5:52:32 PM7/27/15
to elm-discuss, Janis Voigtländer

Max, IMHO that shouldn’t be the responsibility of delay with argument 0, but with a dedicated async function like is described in the original thesis on Elm.

Dobes Vandermeer

unread,
Jul 27, 2015, 7:23:53 PM7/27/15
to elm-d...@googlegroups.com
It seems unavoidable that the delay is only a lower bound ... you can say the system will delay at least as long as given, but not that it will fire at exactly that moment. that's practically improssible and could only be faked if it was provided.  If delay is given a very small timeout it should be free to simply put that in a queue and run it immediately after the current update, without using setTimeout.  But it is also acceptable to run it some time later using a timeout.  Otherwise there's too much constraint on the implementation.

I think there's an argument for saying that if an event is delayed twice with the same delay, they would be lumped into the same signal.  The reason is, as someone mentioned, referential transparency.  Referential transparency means that you should be able to expand variables to their definition, or extract any common subexpression into a variable.  So in ...

bar = delay 100 clicks
baz = delay 100 clicks

s1 = merge bar baz
s2 = merge (delay 100 clicks) (delay 100 clicks)
s3 = merge bar (delay 100 clicks)

s1, s2, s3 should basically be the same signal.  I think.  I'm not clear on whether that is the case currently, though.



--

Janis Voigtländer

unread,
Jul 28, 2015, 1:05:12 AM7/28/15
to Elm Discuss

Yes, I had just forgotten that time is not milliseconds as Int, but rather as Float. So with 1 I really meant the smallest positive number. Which of course 1 is not if we are dealing in Floats.

Janis Voigtländer

unread,
Jul 28, 2015, 1:09:24 AM7/28/15
to elm-d...@googlegroups.com

It is not the case currently. Your s1, s2, s3 will be different signals. But the same is already the case for Time.every and Time.fps. And there it is noted in the documentation. If you have

s4 = Time.every 1000
s5 = Time.every 1000

then you cannot referentially transparently replace s4 for s5 (or for Time.every 1000) in your code.

Dobes Vandermeer

unread,
Jul 28, 2015, 1:42:06 AM7/28/15
to elm-d...@googlegroups.com

Okay, it's clearer how things are. Is this the way they should be? Will future compiler optimization passes have to take these special cases into account to avoid changing behavior unexpectedly, or should they be made referentially transparent at some point?

Andre Medeiros

unread,
Jul 28, 2015, 7:26:36 AM7/28/15
to Elm Discuss, janis.voi...@gmail.com
Isn't it confusing that

a1 = delay 100 Mouse.clicks
a2 = delay 100 Mouse.clicks

are two different Signals which generate a glitch when they are map2'ed, but

b1 = Mouse.clicks
b2 = Mouse.clicks

are the same Signal and don't generate a glitch when they are map2'ed.

Janis Voigtländer

unread,
Jul 28, 2015, 9:01:14 AM7/28/15
to Elm Discuss

Yes, I guess that can be confusing. But it’s just the same as saying that delay is not referentially transparent, which has already been observed in this thread.

There’s not much more that can be done about this (at the moment) than clearly documenting this.

To “solve” it, reliably and in all cases (for arbitrary expressions, rather than just trying to detect special cases with an explicit constant argument to delay), something like global value numbering or hash consing might be needed. I would be surprised to see it implemented.

Oleg Grenrus

unread,
Jul 28, 2015, 9:43:34 AM7/28/15
to elm-d...@googlegroups.com, Janis Voigtländer
I don’t know anything about how `delay` is implemented, but few guesses
- `delay` implementation is tied to the runtime library implementation
- simply uses `setTimeout` or such

I’d implement `delay` (and `fps` and `every`) so:

- they postpone signal updates tagged with `currentTime + delayTime` time point, and `setTimeout`ed at most once per such point.
- `setTimeouted` callback then updates all postpones signals with `currentTime + delayTime` timepoint.

—--

To make semantics correct,
- Time should be internally a tuple of (eventInvocationTime, delayTimeSpan), with equality semantics of pair, i.e. time points are equal only if both of elements are equal
- time addition operation above (+), should be:
  (eventTime, delay) + additionalDelay = (eventTime, delay + additionalDelay)

Above is to make sure we cannot ever delay into signal updates triggered by DOM events.

—--

Currently `delay` is also best-effort:

    import Graphics.Element exposing (..)
    import Time exposing (..)

    f (b, a) = show (b - a)

    main =
      Signal.map f (timestamp (delay 1000 (every 1000)))

doesn’t output constant 1000. It could, but that might complicate a `Time.timestamp` implementation too, as we need to preserve monotonicity (it might be worth pointing are values strictly increasing or not, if not the job is a bit easier). I'd say this change should be done too, for the same reasons: referential transparency and equational reasoning.

—--

The stated above are breaking changes, but I’d really like to see them. If those aren’t done, the composability is ruined.

Currently I cannot define:

    niceAnimation1 : number -> Signal Element
    niceAnimation1 duration = ...

    niceAnimation2 : number -> Signal Element
    niceAnimation2 duration = ... 

    composedSyncAnimation : number -> Signal Element
    composedSyncAnimation duration =
      let a = niceAnimation1 duration
          b = niceAnimation2 duration
      in compose1and2 a b

And reason that `composedSyncAnimation` will be really sync, i.e. no, even unnoticeable, intermediate steps.

Cheers,
Oleg

signature.asc

Janis Voigtländer

unread,
Jul 28, 2015, 10:26:59 AM7/28/15
to elm-d...@googlegroups.com

Further changing the implementations of the Time primitives is not my call to make. :-)

But let me say I’m not convinced all the properties you expect are must-haves, or that not having them “ruins composability”. For example, that Signal.map (uncurry (-)) (timestamp (delay 1000 (every 1000))) should be constantly 1000. What is some real code or refactoring that breaks because of not satisfying this? Also concerning your composedSyncAnimation example, the assertion that you cannot do something desirable there is difficult to judge without knowing more specifically how those animations are supposed to be implemented.

But, that said, I have myself in the past felt the need to improve consistency between timed and timestamped signals. (As you can see by looking at commit history from January, https://github.com/elm-lang/core/commits/master/src/Native/Time.js.) So maybe your ideas could also be implemented and make it into core.

Andre Medeiros

unread,
Jul 29, 2015, 8:54:54 AM7/29/15
to Elm Discuss, janis.voi...@gmail.com
Then it's likely to become a potentially frustrating gotcha in the language. I'd rather have all of them not referentially transparent to make sure map2 is consistent with all cases.

Evan Czaplicki

unread,
Jul 29, 2015, 12:24:22 PM7/29/15
to elm-d...@googlegroups.com, janis.voi...@gmail.com
Andre, probably not the right choice for us ;) 

Signals are not that important these days, enough that I'd not expect people to be using 'delay' really at all. Furthermore, I think this has been in since the first release roughly, when we didn't know how effects would really fit into the picture.

Since we have tasks now, it may make sense to get rid of any functions from signal to signal that have effects and provide task alternatives. In this case Task.sleep
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.


--
Sent from Gmail Mobile

Andre Medeiros

unread,
Jul 29, 2015, 1:12:06 PM7/29/15
to Elm Discuss, janis.voi...@gmail.com, eva...@gmail.com
It's not about delay itself, but about having deterministic guarantees on what happens when there are nearly-simultaneous events on signals.

Take this example:

```
import Graphics.Element exposing (..)
import Time exposing (..)

single =
  every 2000
  |> Signal.foldp (\_ n -> n + 1) 1

double =
  every 1000
  |> Signal.foldp (\_ n -> n + 1) 2

main =
  Signal.merge single double
  |> Signal.foldp (\a b -> a - b) 0
  |> Signal.map (\x -> show x)
```

On Firefox, I see the sequence 0, 3, 1, 4, 1, 6, 2, ...
If I swap the declarations order to be first `double`, then `single`, I see 0, 3, 5, 0, 3, 4, 8, ...

On Chrome, the program above gives me the sequence 0, 3, 5, 0, 3, 4, 8, ... regardless whether the declarations are swapped.

Which of these is correct? Should we fix those cases so they are deterministic? Or should we not use Signal.every either? Should Signals be avoided entirely? (the alternative being keeping timestamps in the model, and doing state management manually. I couldn't believe manual state management would be a good recommendation)

Janis Voigtländer

unread,
Jul 29, 2015, 6:17:46 PM7/29/15
to elm-d...@googlegroups.com
Who said that signally code needs to be deterministic? There are many programs, in many languages, that contain some nondeterminism, specifically if timing/concurrency is involved. That doesn't make those programs incorrect.

So, for your example, all the executions are correct. That some browser only reproduces one of them is irrelevant for this. And if your code or model or thoughts rely on only one of the executions, and thus you have problems when some change (to another, semantically valid execution) appears, then you are working under wrong assumptions.

Dobes Vandermeer

unread,
Jul 29, 2015, 7:15:15 PM7/29/15
to elm-d...@googlegroups.com
Well, I think this may tie back into what are the benefits of FRP, which would be referential transparency and composability and stuff like that.  Otherwise what's the point of switching languages, tools, and so on?  Javascript already has pretty good reactive frameworks with effects, in fact effects (aka Tasks) are even easier to write in Javascript since they are just regular statements.  Using FRP is supposed to avoid all that effecty / tasky / statementy stuff by making the language / runtime take care of it behind the scenes.

I think there are probably reasonable and performant solutions to the delay, fps, and so forth issues ... that would make the system more deteministic, predictable, and composable and thus better fulfill the promise of FRP.  Such as, batching all the delays in an update frame so if they have the same delay they use the same call to setTimeout().  You could even pretend that the elapsed time at the timeout is exactly the time requested and play catch up if things are falling behind, so that if you have multiple timers running that should fire at the same time occasionally (like a 1s and 2s timer running in parallel) they would actually run in sync.

It's solvable and it might be useful.  I would be careful to just dismiss the benefits of determinism just because non-determinism is the default.

That said, I'm not immediately advocating for such determinism, more like pushing back against it being dismissed too easily.  I think this is a question that deserves careful consideration, as it may speak to the original core value proposition of Elm - declarative animations and UI - which may in fact make more sense with deterministic and synchronized implementations of delay, fps, and so on.


Janis Voigtländer

unread,
Jul 30, 2015, 1:04:42 AM7/30/15
to elm-d...@googlegroups.com
Yes, all valid considerations!

Oleg Grenrus

unread,
Aug 11, 2015, 2:03:40 PM8/11/15
to elm-d...@googlegroups.com, janis.voi...@gmail.com
I agree with Dobe. Going away from signals as very sad thing. Signals is what make Elm cool. The Elm Architecture based on signals is so simple! (and deterministic, and nice, and… easy to reason about)!
 
Tasks seems to be asynchronous IO monad, I personally don’t like to put anything there if not really needed.

The real world `delay` usage: to implement `throttle` or `debounce`.

I’m sorry if I didn’t communicate this clearly before: I don’t need this to be fixed /immediately/, I’d just like to see attention gathering warning texts “current implementation has culprits”, “will be fixed when we really have to or we find volunteer,which ever happens first”. 

The non-determinism introduced by those glitches also probably ruins time-travelling debugger, which the selling point for Elm too.

- Oleg
signature.asc

Evan Czaplicki

unread,
Aug 11, 2015, 2:35:14 PM8/11/15
to elm-d...@googlegroups.com, janis.voi...@gmail.com
It does not ruin the time-traveling debugger. Based on the definition of async given in my thesis, these edges turn into output nodes that report to input nodes when they are done. Just like any other input node, we record exactly what goes into it, so it gets tracked like anything else.

Based on that definition of async, goes out and then comes back in, you can think of tasks as a slight extension to this. A message goes out, it is handled by some system, a message comes back in. That's how rendering works. Tasks are the most extreme version of this, but in many cases you can just send out data and have a "interpreter" do what needs to be done with that data (exactly how elm-html, Graphics, and elm-webgl work)

Janis Voigtländer

unread,
Aug 11, 2015, 2:47:20 PM8/11/15
to elm-d...@googlegroups.com

About the issue of documentation/warnings:

Note that the next release of core will come with this warning in the documentation of Time.delay. (And the other functions showing similar non-pureness do already have corresponding warnings attached.)

Oleg Grenrus

unread,
Aug 13, 2015, 8:15:19 AM8/13/15
to elm-d...@googlegroups.com

> On 11 Aug 2015, at 21:34, Evan Czaplicki <eva...@gmail.com> wrote:
>
> It does not ruin the time-traveling debugger. Based on the definition of async given in my thesis, these edges turn into output nodes that report to input nodes when they are done. Just like any other input node, we record exactly what goes into it, so it gets tracked like anything else.
>
> Based on that definition of async, goes out and then comes back in, you can think of tasks as a slight extension to this. A message goes out, it is handled by some system, a message comes back in. That's how rendering works. Tasks are the most extreme version of this, but in many cases you can just send out data and have a "interpreter" do what needs to be done with that data (exactly how elm-html, Graphics, and elm-webgl work)
>

It’s nice to hear that it doesn’t ruin time-travelling debugger. It got me interested to look how it’s made. In case when we go back in time, and do not cause some output node anymore, but want other outputs to be interpreted in the same way still.

> On 11 Aug 2015, at 21:46, Janis Voigtländer <janis.voi...@gmail.com> wrote:
>
> About the issue of documentation/warnings:
>
> Note that the next release of core will come with this warning in the documentation of Time.delay. (And the other functions showing similar non-pureness do already have corresponding warnings attached.)

:+1:

Thanks for the discussion.

- Oleg
signature.asc
Reply all
Reply to author
Forward
0 new messages