Preventing invalid states in update function

In the update function, it’s common to have _ in the case statement to ignore invalid Msg+Model combinations. I prevented this by defining Msg as () -> (Model, Cmd Msg)

https://ellie-app.com/cyRWcG6pYCxa1

is it possible to do this without stale update bug? would this cause bad performance?

What is the “state update bug”?

You can have desynchronization, in certain cases, where the model that was used to initialize the function has not been updated (because the DOM hasn’t had time to be updated) and so you will “work” on a previous version of the model.

By doing it as () -> (Model, Cmd Msg) you are throwing away the version of the model that you should be using, since you are always ignoring the input model parameter. Out of all the ways you could try and make a variation on the usual MVU pattern, this is probably the worst choice.

2 Likes

What’s a concrete example of such a case? I tried to make one with Time.every, but it seems to work correctly: https://ellie-app.com/cz3HYRPqpWka1

Edit: Tried to do it with Process.sleep instead, but got stuck on:

type alias Msg =
    () -> ( Model, Cmd Msg )

being an error:

This type alias is recursive, forming an infinite type!

19| type alias Msg =
               ^^^
When I expand a recursive type alias, it just keeps getting bigger and bigger.
So dealiasing results in an infinitely large type! Try this instead:

    type Msg =
        Msg
            (() -> ( Model, Cmd Msg ))

Hint: This is kind of a subtle distinction. I suggested the naive fix, but I
recommend reading <https://elm-lang.org/0.19.1/recursive-alias> for ideas on how
to do better.

I know for sure we had problems in our codebase related to partial application of model to an update function. Theoretically, it’s going to happen if message A fires and then message B fires before the view where it was created has been updated. So it’s unlikely for simple examples, but could happen more frequently when you have both frequent updates and slow rerendering.
On the other hand, in this talk, Roman Potasovs explains that he creates the update functions in the view and has not encountered any problem: Roman Potasovs Is performance enough in Elm to create full fledged video games Elm Europe 2019 - YouTube

So, obviously, one should do as one wants, but one’s opening oneself to possible bug with one method, and to no possible bug with the other.

1 Like

Here is the Ellie I wrote to illustrate how stale state can happen: https://ellie-app.com/yVC9wPk33xa1

3 Likes

To have Cmd, you can’t use a type alias; do this

type Msg
    = Msg (() -> ( Model, Cmd Msg ))

Coury Ditch wrote a blog post regarding such a stale message bug and I also wrote a blog post regarding this in which the bug manifested itself via auto-fill for forms. That happened because you essentially got multiple field-update messages within one render loop.

4 Likes

is there one that works with touchscreen?

Alexis King has some thoughts on type-safety and wildcards in pattern-matches. It’s somewhat related, and perhaps useful reading :sunny: Names are not type safety

1 Like

Since the example is with an old version of elm, I can’t save a modification, but if you put this is the command in init, you’ll start out with a stale update:

Cmd.batch
    [ Task.perform identity (Task.succeed (Event 0))
    , Task.perform identity (Task.succeed (Event 0))
    ]

This brings up the question: Is there a way to model updates in a way that actually restricts the (Msg, Model) pairs that can be encountered?

2 Likes

Given a Model what Msgs are possible from that model:

allowed : Msg -> Model -> Maybe Msg

You could define that function, then use it while building the view? But you will probably implement it with a wildcard _ -> Nothing?

Not completely unless you use the current model when constructing a Msg. When you don’t do this, all possible Msg values need to be handled regardless of the model’s state when the update function runs.

(0) This is a really good question. Thank you @dta. It gets to the heart of what needs to be done.

(1) I’m not sure if this makes sense, but what if the Model was a union type (or contained a union type)? For example

type Focus = Browse BrowseModel | Order OrderModel

(2) Now ensure that when Focus is Browse the view doesn’t create Order messages, and vice versa.

(3) The lag problem (aka stale update bug) described earlier in this message still exists. (I think it’s a feature of Javascript.) In the view the focus might still be Browse even though in model it is Order.

(4) Having a union type in the model could make it much easier for update msg model to do the right thing.

(5) In our example, if the view send a BrowseMsg when the model state is Order the right thing might be to change the focus to Error. (So now Focus needs a new variant.)

(6) And when view gets an Error it goes into a modal state, and asks the user what to do.

1 Like

Yes, and this is often how I do it.

The Model describes the states of a state machine. The Msg describes the events that can move between states. The state relationship is a subset of the cross product of the Model and the Msg. As its a subset, there is likely to be a wildcard (_ -> (model, Cmd.none) for the cases where no valid state machine transition exists.

Here is an example for a vending machine - which is often used in introductory courses on state machines:

type Model
    = Start
    | Money Int
    | Refunding
    | Vending


type Msg
    = InsertMoney Int
    | TryRefund
    | TryBuy
    | RefundComplete
    | VendComplete

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case ( msg, model ) of
        ( InsertMoney val, Start ) ->
            ( Money val, Cmd.none )

        ( InsertMoney val, Money oldVal ) ->
            ( Money (val + oldVal), Cmd.none )

        ( TryRefund, Money val  ) ->
            ( Refunding, refund )

        ( TryBuy, Money val ) ->
            ( Vending, vend )

        ( RefundComplete, Refunding ) ->
            Start

        ( VendComplete, Vending ) ->
            Start

        ( _, _ ) ->
            ( model, Cmd.none )

This code was directly derived from an initial pen-and-paper sketch of the state machine.

In the above example, if someone was to insert money and then press both the ‘vend’ and ‘refund’ buttons at the same time, one will get processed before the other. This will put the machine into either the Refunding or Vending states. In those states, the other button action will be ignored (stale update). This prevents someone from getting both the product AND a refund with some cheeky action on the buttons.

This is an example of how putting a state machine on an asynchronous event stream transforms it into an ordered stream of valid events only. The real world is always going to be asynchronous and it is usually the job of a UI to tame that real world event stream, and state machines are an ideal way of modelling and implementing that. Again - similarities with design methodologies used in hardware definition languages.

4 Likes

I updated the example to 0.19 and added that to init:

https://ellie-app.com/cBbWsmsGpbta1

1 Like

Is there a way to make sure the updates aren’t too frequent?

That isn’t really the problem.

There is a queue of events that gets fed to the update. The view can place events onto that queue. The view is updated with the animation frame - so it only gets a new copy of the Model around 60 times a second. If events fire quickly from the view, then 2 or more events can be built against the same Model version.

It does not matter how quick or slow the update runs, it just pulls events from the queue and processes them. If some events are stale, its not because of the update function, but the view that put them there in the first place.

The solution is to not capture state from the Model in your Msgs. So instead of

type Msg 
    = MoveCursorTo Int -- Absolute value based off model. (value = model.pos + delta)
    | ...

Always work with deltas against the Model:

type Msg
    = MoveCursorBy Int -- Relative to the model. (value = delta)
    | ...

Then the problem is solved.

Unfortunately, I don’t think there is an easy way to use types to enforce this design pattern. Maybe something along the lines of:

type Position = ...
type Delta = ...

applyDelta : Delta -> Position -> Position

makeDelta : Int -> Delta

extractPosition : Position -> Int

So make Position opaque and only provide constructors for Delta and the ability to apply deltas. Worth doing? Not so sure.

2 Likes

(0) Here’s my understanding of the situation. Disclaimer. I know something about logic and formal methods, but am a newcomer to programming Elm. If I get something wrong, I’ll learn something new.

This is a long post, in part because I first summarise previous valuable statements from other contributions. Steps (11) and (12) give a demo of some key ideas.

(1) There are two problems here that require a solution. One is the user experience. The other is the design and implementation of the app.

(2) There are two states (of the Model) here. One is the state that was passed to view to generate the HTML that the user sees (or screen reads etc). The user can change that HTML, by typing into a text box. Call this view_state.

(3) The other state is the one most recently returned to Elm by the update function. Call this the update_state.

(4) Due to lag and timing issues, the two states may not be in strict lockstep. Relevant (and surprising) here is Lockstep - Wikipedia.

(5) Suppose the view_state contains a PressOnce button that vanishes after it is pressed. If the user is quick, or is a robot, the view can send two PressOnce messages toupdate.

(6) Here’s the result of Never Trust User Input - Google Search.

(7) The update method in the Elm app should have an appropriate level of mistrust of the messages sent to it from the view generated HTML. Each state and so each view_state has a version / sequence number.

(8) Having the view HTML return this version number as part of every message might help the update message to be appropriately suspicious. The update could treat the version number as a security token which can expire.

(9) What is appropriate user experience depends on the context. In some situations, presenting a Modal window - Wikipedia is the right thing to do. Wikipedia says

A modal window creates a mode that disables the main window but keeps it visible, with the modal window as a child window in front of it. Users must interact with the modal window before they can return to the parent application.

(10) A banking app might present a modal window whose HTML can generate only two messages, namely Confirm or Cancel. This might be inappropriate in a gaming app.

(11) While typing this message, Discourse shows at the foot of the page an OK, Buffering or Blank icon. Perhaps this is a visual indication of the the mismatch if any between what I’ve been calling the view_state and the update_state.

(12) If you draft a reply to this message, you’ll have a demonstration of what I mean. You don’t have to send the draft message.

1 Like