Limitation/Flaw of TEA ... is there a fix?


#1

I’ll assume readers are familiar with TEA. We were shown by Evan, in elm-sortable-table, how TEA can be used to create a “component” (and by component I simply mean a smaller part of a larger app).
Richard Feldman’s canonical SPA example uses this same TEA style within Pages.

We are led to expect that we can compose an app of Pages and components, which themselves may contain sub-pages or components, etc.

If each component has it’s init, view, update functions, and an internal model with standard type signatures, then everything should be fine if we follow Evan’s advice here.

I’ll quote the advice here as it’s very important, and the emphasis is Evan’s.

The data displayed in the table is given as an argument to view . To put that another way, the Table.State value only tracks the details specific to displaying a sorted table, not the actual data to appear in the table. This is the most important decision in this whole library. This choice means you can change your data without any risk of the table getting out of sync. You may be adding things, changing entries, or whatever else; the table will never “get stuck” and display out of date information.

But what if “the details specific to displaying” are a function of “the actual data to appear”? In other words, our model is a function of what occurs in view.

Might this be a problem? Can it be avoided?

We might have:

type alias Model = {
   manySubWidgetStates : List SubWidget.Model
}

view : {Parent.Model : parentModel} -> Model -> Html Msg
view {parentModel} model = 
  parentModel |> expensiveGenerateSubWidgetData |> List.map viewWidget |> ...

Here view calls an expensive function to determine which sub-widgets will be rendered. But their cmds will be processed by update which we expect will be ready with manySubWidgetStates. How?
This will only work if update runs expensiveMapToSubWidgetData and stores the result.

So, we need to guarantee that update has “fresh” data and that it perform’s the majority of view's work. Is this a problem? Yes.

First, view is special in that we call it exactly when we need it, with up-to-date data in our hands. We can’t help but call it with the appropriate data at the appropriate time.
update is a little different … if the parent gets a Cmd wrapped up for our component, it’ll certainly have to send it to our component’s update, or very conspicuously throw it away. But, if we write/modify an update and just hope that it will be called every time our parent’s model changes in some way, it could be very easy for the parent to accidentally not call our update. And since our update is doing work on data that is later supplied to view, we break the most important decision in our app and risk data being out of sync.

Have I missed something? Is this avoidable?


#2

Can you clarify this? How could the model be a function of the view?


#3

If I understood correctly, your concern is that if the parent update (the global update function) doesn’t call the widget update (the update function exported from the widget module) or if it calls it incorrectly then the view could break / not change.

If that’s the case, that is indeed a limitation of the architecture. The solution is to make your widget as easy to integrate as possible, preferably only have the caller add a single case WidgetMsg msg -> Widget.update msg model line to their code to make it work. If the widget depends on the parent model then the API should be again as minimal and well documented as possible, types help a lot in this case.


#4

Hmm, the “config arguments” don’t have to come directly from the model or be static, they can be computed as a function of the model too, and Html.lazy can be used to avoid too many recomputations.

For example, I have a basic sortable table component, and for one of my pages I’ve wrapped it in a function that adds search with highlighting and toggleable columns. Which columns to show is stored directly on the “page model” rather than the “table model”. It’s all “clean” with no caching of derived values in the model. I did start to see a little bit of a slowdown, but lazy fixed that.


#5

So, in Evan’s component document, he argues that there should be a single source of truth.

If, in my view, I decide based on data passed in that I’d like to render X, and I need to initialize a model for X (to send X’s messages to), then it’s too late! Although I can initialize X.Model in a view, I can’t update my model with my X.Model.
So, I must pre-initialize X.Model in my update, so that it’s ready for my view.
If I’m initializing something in update, that I could also initialize in view then I don’t have a single source of truth. Evan says “Elm is built on the idea that there should be a single source of truth”. But it seems impossible! Therefore, as I suggested, there is a problem with TEA.


#6

Hmm, rather than talking about view and update specifically, could you give a concrete “real world-ish” example of a feature that you think is impossible to implement cleanly?


#7

Suppose my main model is a relational database and a query string. The database gets updated over the wire, as well as from the user.

Now suppose we have ResultsPage.view : String -> DB -> Html ResultsPage.Msg. So the results page applies the query to the DB to get result.

Suppose further, that we have a RowEditor that lets us edit the column values, but also lets us rollback etc before we commit. Perhaps it has some toggles do display data as various formats. It’s complex enough to warrant “componentizing” into a RowEditor Model/init/update/view.

We can’t simply go ahead and start using these RowEditors in our view, because when we call ResultsPage.view, and it wants to call RowEditor.view, it’ll need a RowEditor.Model which we don’t have yet.

Ok, we’ll need to fix ResultsPage.update to run the query on the DB, and generate appropriate RowEditor.Model values so that they are ready when RowEditor.view is later called from ResultsPage.update.

So, in ResultsPage.update we are deriving data (query results) and storing data (RowEditor.Models) which we can also later derive (but not store) in ResultsPage.view.

If you are deriving and storing data which you can later derive, then you have broken the “Single Source of Truth” principle. Don’t store data that you can derive.

Evan said: “Elm is built on the idea that there should be a single source of truth.”

Ok, so what are my options for a fix?


#8

Ah I see! How about this? :slightly_smiling_face:

results |> List.map (\row -> RowEditor.view (model.editors |> Dict.get row.id |> Maybe.withDefault RowEditor.init

If the state of the RowEditor would be the same as RowEditor.init, then we don’t actually need to store it in the model. Only once the user interacts with the editors do we actually need to store their state!


#9

I have considered this, and I’m already using it. And it’s the best solution i know so far.

There are 2 issues:

  1. If a subcomponent’s init takes a parameter based on the model, it’s problematic.
  2. We have a memory leak. We create models dynamically but don’t t dispose of them.

Another possibility is that the subcomponent’s update requires the necessary parent model data. So we cant be sure a subcomponent’s update has been called with the most recent data, but we know when update is called the sata eill be up to date.

In any case. Theee is no solution without caveats.


#10
  1. How is it problematic?
  2. It’s up to you to decide when you want to dispose of them. I don’t think that’s a bad thing, having state destroyed/reset because of the view isn’t necessarily good (which happens with state stored in React components for example, although Vue has a special keep-alive directive to prevent this).

Another neat thing you can do is extend the update function with a “watch” step, so that after it has handled a message, it takes a look at the model and updates it / issues commands based on some parameters.


#11
  1. is a problem because we’ve decided to lazy init something requiring model data on the NEXT update cycle when the data might be different. So the model will not be initialized to the render-time data. May or may not be a problem depending on app logic.
  2. We’re lazily creating models based on messages. But cleaning it up based on model data. I don’t love this idea, but I suppose it’s doable.

So it seems the best (only?) strategy for an Elm app with components/Pages is:

  1. lazily generate Page/component models based on messages that expect to be sent to said model
  2. don’t write a model/page initializer that accepts an input to be derived from a parent model
  3. be sure to clean up sub-models on all updates

#12

Ive read through this thread, but I dont see a problem.

For a big application with a relational data base, that depicts a bunch of rows and columns, and can toggle some rows to be editable; I can think of two ways of going about it:

  1. You treat the database as the single source of truth, and when RowEdit.Msg come in, they edit that one Row directly. Like this:
type alias Model =
    { database : Dict String Row }

type Msg
    = RowEditMsg String RowEdit.Msg

update : Msg -> Model -> Model
update msg model =
    case msg of
        RowEditMsg key subMsg ->
            case Dict.get key model.database of
                Just row ->
                    { model
                        | database =
                            Dict.insert 
                                key 
                                (RowEdit.update subMsg row) 
                                model.database
                     }

-- RowEdit.elm

type Msg
     = NameFieldChanged String

update : Msg -> Row -> Row
update msg row =
    case msg of
        --..
  1. Maybe if the data is a representation of data stored remotely on a server somewhere else, the process of editing is kind of arduous and involves verification from the remote server. You could spin off RowEdit into its own Model, that will be initialized from a specific row, but co exist with the data it was created from, until it gets the OK from the remote server that the edit really occured.
-- Main.elm
type alias Model =
    { database : Dict String Row
    , editor : Maybe RowEdit.Model
    }

-- RowEdit.elm

type alias Model =
      { nameField : String
      -- ..
      }

init : String -> Row -> Model
init key row =
    { key = key 
    , nameField = row.name
    -- ..
    }

One might claim that this is a violation of “single source of truth”. I dont think it is tho. The row itself has a single source of truth (the frontends “database”), and the row edit has a single source of truth (the RowEdit.Model). Two distinct things in the program, with two distinct sources of truth. I know they bear the obvious relationship that one is an edit of the other, but by that very same relationship they are distinguished from each other. A row in a database and a UI for editing a row are simply different beasts.

I dont want to put words in your mouth tho. If my examples dont address the problem you are trying to show maybe you can point out how.


#13

Yeah, I think this a bit of a fundamental difference between “backend” thinking and “frontend” thinking.

The model in memory is the source of truth. Not the database.

Once you try to stick a layer between the model and the data and say that something external is the source of truth, it breaks the whole conceptual idea of TEA.

For example, you can’t say that an external database is the source of truth. You can only say that data returned from a DB then loaded into a model is the source of truth, because obviously other folks could be updated the DB, the query could die, the network could timeout, etc.

Until the data is actually sent over the wire, parsed, and the model is successfully updated, it can’t be the source of truth.

The same applies to user input. Until a user does something to indicate that they want the model updated (e.g. onClick, etc.) and the model is actually updated, you can’t treat that data as a source of truth.

It is up to the update function to reconcile how to deal with those two things, e.g. potential DB updates & potential user updates to the same data set.

The actual model-update event loop has to be single threaded, so you only have a single value change to the model per update cycle (which is why the time-travel debugger works–only one thing changes per update cycle!). PS this is why the whole concept of “components” often falls apart–you can’t have “component” data with TEA. Ultimately, all data needs to be in the model somehow.

One way or another, you have to have a single source of truth for all of the data a user sees & interacts with in an Elm app, and that has to be local to the Elm app & in memory, e.g. the model.

That is totally different from a backend, where many processes are competing to update data, etc. and it’s up to the DB to reconcile & manage those simultaneous requests.

Make sense???

I don’t see any “limitation or flaw” in TEA. I see this is a huge benefit, as by definition nothing can ever get out of sync–there can’t be two things competing to update the same thing at the same time in TEA.

To get back to your original point:

That is impossible. You would never want this, as it’s recursive. You can’t have:

functionA a =
    functionB a

functionB b =
    functionA b 

What you CAN have is:

Our view updates our model, which in turn changes the view to reflect the newly updated model.

view does only two things:

  1. Reactively renders a model
  2. Potentially passes commands to the Elm event loop to update the model (which could potentially cause the view to be re-rendered).

The flow is: init model -> view model -> update model -> view model -> update model etc.

You can’t have a model that is a function of view directly (nor would you want to!), view can only render a model, then potentially send commands to update a model, which will in turn cause the view to be re-rendered (as the view is reactive).

Rock on,


#14

@madasebrof you’ve grossly misunderstood my understanding and experience with Elm and thus you haven’t engaged fully to understand the problem, believing instead I need an Elm lesson.

When I said

our model is a function of what occurs in view.

I meant figuratively. Our view determines what we see, but if what we see will trigger events, our model will need to be prepared to handle them. In that sense, interests of the view dictate interests of the model and if changing our view code therefore requires changing our model code then our model CODE is a function of our view CODE, or in my misunderstood shorthand, our model is a function of our view. This was further eluded to.

As a fun tangential aside, you can actually can get very close to LITERALLY having the model a function of the view.

It’s possible to create a custom element that does connectedCallback() {this.dispatchEvent(new CustomEvent('attempted')); and render it with an Html.Events.on "attempted" (Decode.succeed msg) such that the view : Model -> Html Msg code can trigger an immediate model update by simply rendering.

I actually do this and it’s fine. It’s to have a better behaved implementation of https://package.elm-lang.org/packages/elm/html/latest/Html-Attributes#autofocus.

And, as you probably know, recursion (mutual or otherwise) is fine so long as it terminates. :wink:

In any case, please re-read everything I’ve written before, as it was enough for Herteby to see what I was getting at. Ask me clarifications if you don’t understand something. I don’t need any Elm lessons.


#15

Yeah, sorry. I re-read your post like 5 times before I replied. The I reason I replied the way I did is that the title is of your post provocative as it presupposes a fundamental problem with TEA.

I am just saying, in fairly plain terms, that there is no problem with TEA so that future readers don’t get confused, and that, unless I am missing something, that there is no problem to fix.

If there is an actual issue you are having with Elm, I apologize for not getting it!

I wrote what I did in all sincerity (not to be patronizing), as I have seen many such posts by folks who come from other architectures like Vue, React, etc. and want to try to make “components” in Elm. It took me a while to get this myself!

Back to what you wrote:

To me, there is nothing that complex that could warrant doing what you describe.

I’ve posted this before, but here’s my highly opinionated way of writing big Elm apps so you can see where my head is at:

This is how we write production code. Hope this helps! If not, again, apologies for not getting it. I’ll let you continue the discussion w/o me!

Best,


#16

After reading elm-taco-donit it seems that you and I have different interpretations and ideas of how Elm apps should be built.

I believe this ‘nested component’ paradigm of organization fights TEA (The Elm Architecture) and makes for code that is difficult to maintain, write and comprehend.

This ‘nested component’ paradigm follows directly from Evan’s Table code and Richard’s SPA example.
Pages are components nested in Main, and Tables (for example) are components nested into pages.

Also, apart form the single issue I (attempt to) bring up here, my code is not difficult to maintain, write or understand. Quit the opposite. So, I have reason to believe nested components are a net good.

BUT, let’s suppose your way of building apps is best (and I’m fully admitting that it could be!), then it seems to follow we aren’t allowed to have reusable component packages. That in itself seems like a limitation/flaw with TEA.

In any case, we either run into the issue I’m trying to present, or we run into the issue that we can’t have reusable component packages.

Therefore, I remain convinced that there is a limitation or flaw with TEA.


#17

All good, but just to be clear (as we use Evan’s table thing) it is a view component just like anything else. The data is stored in the model, not within the component itself. To render it, you pass it the model. It doesn’t ‘self render’ its own internal data.

My who rant is basically just about taking Evan’s table model and applying it to big apps, e.g. single source of truth for all data in a single model, with lots of view component that can render parts of that model in many ways.

Who knows what’s best. It works for us!

Ps I did look at Richard’s Elm-spa example. God bless Richard, but I would for sure re-factor and flatten! It’s not even that much code! LOL

To each his own!

:peace:


#18

I’ve been following this thread closely.
Can I ask a clarification question? What do you mean by the word “component”?

Background:
In a talk from Evan he referred to components as basically the equivalent of objects in OOP. They hold both logic and data. That can obviously be true for any React component, but that is impossible to express in Elm, and the Table and the Pages in Richard’s SPA are definitely nothing more than view functions or at most a collection of functions. They are not, in this sense components.

In my view, you are right that it follows then that we are not “allowed” to have reusable component packages, simply because that cannot be expressed. We have a whole lot of functions to share, some of which are functions that happen to return Html. But that is not a flaw of anything, just like a function from mathematics cannot hold data, it can only map data, so do elm constructs. Isn’t this the root of functional programming?


#19

I read the OP and several other clarifications in the thread, several times, and I still don’t understand what the alleged problem is, let alone how I might go about proposing a solution.

At this point it seems best to start a new thread explaining the real-world design problem from which this concern originated. (I’m assuming there was one.) I’d begin with the sentence “I have been building an application that does _____ and I have encountered the following problem:” and go from there.

Both Evan and I have spent a lot of time explicitly saying “thinking in terms of components in Elm is a mistake.” I understand that you disagree with us, based on:

Please don’t tell people that this belief “follows directly” what Evan or I have been saying on the subject.

We’ve been super clear that we disagree strongly with this.


#20

@rtfeldman

When you say “thinking in terms of components in Elm is a mistake”, you do first say

components = local state + methods
local state + methods = objects

So that’s your definition of component (and I’m franky not sure how you get to decide that). My definition of component, (and it’s the original one found in every dictionary) is: “a constituent part”. A logical unit. Certainly one can define component however you like and draw consequences from one’s definition.

By my definition (and the dictionary’s, AND probably many engineers) a module with ( Model , init , update , view) IS a component, and with that definition, it’s something you’ve both argued for.

So my belief does follow directly from what you and Evan have been saying (except you’ve chosen a problematic definition for component).

I expected this to be clear when I said, in my first sentence

and by component I simply mean a smaller part of a larger app

and later

… component has it’s init, view, update functions, and an internal model

Edit: catch me on slack, @z5h if you think it’s better to discuss there, and post our final ideas here.