Nested views problem: the "versioned views" use case

Hi. I want to expose here an analysis I’ve been doing of a use case I have at work, and the conclusions I’ve taken. To be honest, it started as an open question, but while explaining I ended up finding a solution that feels right for me. But I still want to share it and validate the conclusion with the community.

The simplified version of my use case is the following:

  1. An SPA application with several views
  2. In the application there are views that are only a function rendering some data (like checkbox example). There are also views that have internal state and specific Msgs that mutate such state (like elm-sortable-table)
  3. The application needs to be able to manage different kind of assets. Depending on the type of asset managed, the views might have a different representation, different inputs and a different behaviour (ie have a different model, different Msgs, different view). It can be reduced to: the views can have multiple versions
  4. All views must use the same version, therefore the knowledge of which version is used must be part of the Model of the root view.
  5. Only one version of each view will exist at a time, therefore it feels like we should only hold the Model of one of the versions at any given time (Model = V1.Model | V2.Model). This is my understanding of “make impossible states impossible”, although I might be misunderstanding it.

The problem is: Following the standard Elm Architecture pattern, premise 5) cannot be achieved without compromising the consistency of the state of the application.

I have some example code.

Now, my questions are:

  • Does anybody disagree that the final solution is indeed, the only solution? Would you solve this use case differently?
  • In the code of the final solution, I’ve deliberately leaved the function update untouched and called it on the onClick event, to show the difference with the standard pattern of The Elm Architecture. Does this feel as an antipattern to anybody?
  • Is this a valid evidence to support the case that the standard elm architecture pattern is only valid for the root module of the application, and an anti-pattern for subviews?

Hi,

Kind of difficult to understand what you are really trying to do, by looking at a contrived example made up of A1 and A2, but…

My initial thought is that there is no difference between your versions and different pages in an SPA. You pages A1 and A2 have different models, different views, and take different messages. You could describe them as version 1 and 2 of A, or you could just describe them as 2 entirely separate and unrelated pages of an SPA.

In which case, you can just wire them up within the Main module, as nested TEAs, exactly as the pages of the SPA example are.

You are trying to hide the fact that there are 2 version of A from the Main module, abstracting their messages as the type A.Msg. But then you need an intermediate module, A, that reveals A.Msg to be either A1Msg or A2Msg. At some point you have to state what the types of messages for A1 and A2 are, so I don’t know if you gain much by trying to hide this, only to have to deal with it in an intermediate module.

No, there are always more ways of doing things.

It is not the simplest pattern for sub-views, and other patterns should be tried first. However, once you reach a certain level of complexity and your sub-views really start to need to manage their own state, then it becomes inevitable. We can’t describe something that is often necessary as an anti-pattern; just a pattern that we should not reach for straight away.

===

If your versions had more in common, there are ways that you could present them so that you can have different implementations of the same functionality.

For example, suppose I have multiple versions of something that render some Content. When new content is loaded from the back-end, the Model needs updated to incorporate the new Content. I could define an API like this:

module ContentPage exposing (..)

type alias ContentModel model = 
  { addContent : Content -> model -> model
  , changeColor : Color -> model -> model
  , changeSize : Size -> model -> model
  }

module A1 exposing (..)

init : ContentModel A1Model

module A2 exposing (..)

init : ContentModel A2Model

Now both versions have a common API, with nearly the same type. So either version can be substituted, and the Main.update function can manage both versions without knowing too much about their internal details (it does need to understand that the types A1Model and A2Model are distinct though).

You could then write a more generic update function that abstracts over the model type parameter, but can process messages for either version:

type Msg = AddContent Content | ChangeColor Color | ChangeSize Size

update : ContentModel model -> Msg -> model -> model
update contentModel msg model =
    case msg of
        AddContent content -> contentModel.addContent content model

        _ -> -- You get the idea.

Can you share some code?

Apologies, I was indeed too imprecise in my initial explanation :sweat_smile:. Let me explain the use case in more detail: imagine an SPA application in the IoT business. This SPA application has 3 pages: a page to administrate devices; a page to see the current state of the sensors of one device as a visual representation with the option to interact with it (in short: HMI page); and a page that shows a dashboard with historical data of one device.

This application is able to load at the same time different types of device: fridges, windows and televisions (this is a made up example, please don’t go all Internet of Shit with it :joy:). Imagine the application has many more types of device, and there are many more to come in the future. There’s no difficulty in implementing the administrative page generic for all types of device. There’s no problem in implementing the historical page generic for all types of device. The conflictive page is the HMI page.

Imagine the HMI page as a page showing an SVG, with the value of the sensors rendered in it, and some interactive controls that allow to remotely actuate on the device. Each of these controls is a side effect that needs to be inevitably managed with some specific property in the Model and some specific Msg. Each type of device will have a completely different svg, different layout of sensors, and different controls (different in number and nature). In order to scale the organization of the code properly, we want to implement each HMI page for an specific device type to be implemented in its own module (these would correspond to A1 and A2 of my contrived example). However, each of these modules is definitely not a different page, it is a version of the same HMI page. It is for this reason that in the model where we have the different pages of the app, what feels more natural is:

type alias Model =
    { adminPage : AdminPage.Model
    , historicalPage : HistoricalPage.Model
    , HmiPage : FridgeHmi.Model | WindowHmi.Model | TelevisionHmi.Model
    }

Which is what I meant by premise 5) in my previous explanation.

I hope this clarifies my use case and the problem that I tried to reduce with the A1 & A2 example.

==

Yes. Please forget about that indirection, it’s not something I necessarily like, and it’s orthogonal to the problem. The example code just came up like this :sweat_smile:

1 Like

Yes, this is an antipattern. Sending replacement models rather than messages describing changes to be made to the model has horrendous problems the moment you introduce any sort of asynchronous behavior — e.g., commands.

Mark

I’ve been trying to refute your statement for a while but I have not succeeded. Therefore, unless anybody else can refute it, I take it for true.

This is the reasoning that right now convinces me the most. I realise now that we have an slightly different understanding of how to manage pages. In fact, the Elm Spa Example from Richard Feldman seems to accomplish exactly what I need, so I definitely need to study that code in depth before continuing with this discussion.

1 Like

No, it is not. This is precisely the pattern used in elm-sortable-table.

Do you have an example of this?

I agree with @MarkHamburg here. For tiny, isolated pieces of state that deal uniquely with user-input and allow you to essentially think of them as being a synchronous action like elm-sortable-table, it works well enough.

Imagine an interface with 2 buttons: 1 that loads a list of cat-gifs to show on the left-hand side of the screen, another one that loads an image of a spoon to show on the right hand-side. The data would be loaded by an HTTP request.

In the proposed setup, there is only a single type Msg = Msg { updatedModel : Model, cmds : Cmd Msg }. Now, imagine we click the button to load the cat-gifs, and before the HTTP request is finished, click the button to load the image of the spoon. What will the user end up seeing?

It would depend on which HTTP request would finish first, but they would not see both the cat-gifs and the spoon at the same time.

https://ellie-app.com/FWSyhmKHa1/0 for a sort of simulated example.

1 Like

This is not realistic. It violates current recommendations regarding the structure of the Msg (data only).

One can shoot oneself in the foot with or without the pattern.

Here is the example you provided with a similar foot-gun but without the pattern discussed above:
https://ellie-app.com/XY35mGkLa1/1

Exactly my point. Not an anti-pattern but one of the many tools available. Now if somebody uses it in a way that is inappropriate, then it stands to reason that it will produce suboptimal results. I think it is a very nice pattern for small accidental state like the one in elm-sortable-table.

When you send tiny isolated pieces of data — like the settings for sortable table — that send is basically a delta to part of the model. Now, if that’s the totality of your model, then it’s all one and the same. Similarly, if your model were just a single text field value, a SetInput message would be a full model replacement. The point, however, is that when the model gains more elements, we don’t want to build a new model in the message send — e.g., SetModel { model | text = newText } — because that is now in a race condition with anything else trying to modify even other parts of the model — rather we want a command SetText newText that will be serialized by the message queue and applied in the update function. The more general rule could be summarized as “don’t compute new model state in building your messages but rather compute it in the update function.”

This is also a reason not to include functions in messages (independent of whether the debugger likes them or not): it is way too easy to capture the state of the model at the time the message is constructed rather than working with the state of the model at the time the message is applied. This is a case where you are guaranteed to fail when asynchronous messages come into places, but rather one where “incorrect” code will look enough like “correct” code that it is easy to write the wrong thing and a code review can easily miss it.

Mark

I would generalize this as: “don’t compute new model state in building your messages unless it is a very small model”

Of course this pattern does not work if you abuse it. Another way this can be abused is if there are a lot of possible events for the view and the new model is computed for all of them. This would be highly inefficient.

BUT, in spite of these drawbacks, I do not think this is an anti-pattern.

To me, an anti-pattern is something that should be avoided due to it being highly counterproductive in a more general sense. This pattern is very useful for small accidental state and that is where I use it.

If not an anti-pattern, how about a code smell? Stinky cheese can taste good, but in general something that smells bad is a sign of trouble.

Mark

That would be like saying that Evan writes smelly code. I would not say that. :slight_smile:

I am not disagreeing with the fact that this can be abused and turned into a monstrosity. I’ve done it myself, I know it can be done. :slight_smile:

The only example I know of from Evan is sortable table and that can be viewed just as readily as apply this delta to the sort criterion which is hosted within the broader model. I would argue that sortable table is basically an example of how to write views that are independent of models.

Mark

elm-sortable-table has a model (state) for its accidental state. It cannot be independent of it. What that approach provides is a way to be independent of calling update on a child. That’s the only difference. You still need to provide the view with a model and you still need to provide the view with a mechanism to handle its events.

After looking at the Elm Spa Example more closely, these are my conclusions:

Despite some minor differences, the essence of the solution chosen in elm-spa-example is my attempt 3. I discarded it because the consistency of the model is not 100% enforced by the type system (not 100% impossible states impossible). Is it good enough while we wait for a potential improvement of SPA navigation in 0.19? Yes, it’s probably not a big deal. Although given that we are choosing less than ideal solutions, Depending on the case the simplicity of Attempt 1 should also be considered.

It is an invalid pattern once you have asynchronous Msgs. Whether this makes it an anti-pattern is up to the reader.