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
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
The relevant lines in the runtime source code are here:
It has to do be called on every
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.
Just because this is relevant to something I’m working on right now, when exactly is update called? On the receipt of any message?
Yep it’s whenever a message is received. On every message, the following things happen in the section of code I highlighted above, line by line:
update function is applied to the message and the previous model, returning the tuple
- An update to the view is scheduled for the next animation frame
cmds is extracted of the tuple that was returned from
subscriptions function is called on the new model, to give the new
subs are dispatched to all the effect managers (that includes ports,
Http, and anything else you’ve got in your app.)
Here is an Ellie that checks events generated by a
Mouse.move subscription are not stale:
So far, I have not been able to generate a stale event from a subscription.
Here is an Ellie that checks if events generated from a
view are ever stale:
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.
Ok, this one does show that
subscriptions can generate stale events:
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 =
type Msg =
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.
This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.