It is possible for update to be called many times in between calls to view. That means that messages can sometimes be built against a stale version of the Model. It also means that messages can be processed asynchronously for performance; imagine if update and view had to run in lock-step.
It occurs to me that I do not know how the subscriptions function is invoked in the TEA runtime loop. Does it get called after every single update invocation? Or is it tied to the animation frame like view? Or something else?
I’m curious to know if subscriptions can ever be out of sync with the Model, and whether this can lead to spurious events? I don’t have a specific bug I am trying to fix; just wondering.
Yes, it’s called on every update.
The relevant lines in the runtime source code are here:
It has to do be called on every update because model is actually an input to subscriptions. In most apps, that input is ignored, but you can actually change your subscriptions based on the model.
The reason the view is only updated on an animation frame is that the browser can’t render things any faster than that anyway, so there’s no point.
But events can happen any time, and the app state needs to keep up with them. That includes the subscriptions.
So in other words, it’s done this way to avoid just the kind of problem you’re thinking about!
I wonder if >1 event can be fired from subscriptions against the same version of the Model? There would seem to be much less chance of that happening than compared with the view due to the subscriptions being updated immediately, but I still suspect it can happen.
I shall make an Ellie with a mouse subscription to see if can happen.
It is easy to get stale events from a view, due to the animation frame delay.
I don’t think this conclusively proves that subscriptions can never generate stale events, perhaps I can somehow increase the rate of event generation to the point where I do get stale ones.
By subscribing to key downs and mouse movement at the same time, it is possible to generate events close enough together that the asynchronous behaviour becomes apparent.
This is the behavior as coded rather than the behavior as documented. Since the documentation is silent, the coded behavior could change at any time.
The argument that subscriptions needs to be called for every update because subscriptions depends on the model would also imply that view needs to be called for every update since it too depends on the model. Subscriptions should, however, obviously be called frequently since it does depend on the model and hence updated model does mean that we may have updated subscriptions,
One implication of this is that, whether it is called on every update as 0.18 does or merely called incredibly frequently, an expensive subscriptions function can hurt.
As for the question of stale messages that seemed to be driving some of these questions, if you consider that an effects manager can dispatch multiple messages at once based on the current subscriptions, those messages essentially have to go into a queue and there isn’t sufficient identification information available to stop the delivery of later queued messages if earlier queued messages have changed the subscriptions to remove interest. (Though again, the key point here is really that the documentation is silent and hence the behavior is subject to change.) In other words, subscriptions should be approached with the same sort of general precautions as one needs to take around async messages in the presence of potential loss or refocusing of interest.
This idea of ‘behavior as coded rather than behavior as documented’ is familiar to me from the days when I used to write high performance messaging software.
We would code our system as a series of processing stages which could be combined together synchronously or asynchronously. Synchronous was usually better for low-latency which was the main use case, but async was better for throughput which was sometimes the more important use-case. It didn’t really matter either way - you still assumed the behaviour was async and coded for that expectation - that would work correctly in both scenarios.
I find in Elm I am very often implementing my update around a state machine, and tagging the messages against the states they are legal in:
type State =
Loading
| Ready
type Msg =
HttpResponse
| MouseMoved
update msg model =
case (msg, model.state) of
(HttpResponse, Loading) -> ...
(MouseMoved, Ready) -> ...
_ -> (model, Cmd.none) -- ignore as no-op
That way if multiple events are in flight at the same time, and an earlier one puts the state machine into a state that is not allowed for the later event, the later events will end up being ignored. I assume everything can be async, and use a state machine to restrict to the allowable models.