Philosophies that good functional programmers from the Elm and Clojurescript communities should agree on:
- An application should have a single source of truth -- all your model data in one place.
- Your model data structure should not depend on your interface; a data model should be organized in whatever way is good for the sake of the data, only. View structure != model structure.
- View functions should receive only the data they need for building themselves. Whole model state should not be passed freely everywhere, as this gives most views much more information than they need; this would also make it difficult to build re-usable components, if view functions must also query deeply into a custom data store in addition to performing view logic. Beyond this, it creates a conceptual burden on developers when all views are handling whole model state, you cannot easily follow the flow of data access.
- Related to 3), a view should not inject a data dependency into its parent. That is, all the view nodes/functions of a large hierarchy should not require access to lots of extra state just to support distant views far down the hierarchy.
How to achieve these goals in Elm?
Example:
This is a dramatically simplified example, so assume that the issue is quite magnified at a large scale.
We have an app where a user sees different views and analytics related to GPS data. They might occassionally share their analysis with third-parties. The app offers in-depth ways of aggregrating and manipulating GPS data in the form of various widgets, which can be customized and saved.
Suppose after logging into the web app, the app syncs some saved information from a server and then the app session begins with a single app state that looks like this:
(a single record)
GPS-websocket-data: [... live streaming data is dumped here immediately ...]
Friends: [... third parties and other users connected to this app's user, for sharing out analysis ...]
Widgets: [... all the saved analytic widgets and their user customizations. these are modules that the user can place anywhere...]
Screens: [... customized layouts with different saved widgets placed onto them. the same widget might be on different screens...]
Current-Screen: a flag noting the current layout the user has onscreen
In reality, such an app state would have much more than this with many more pieces interacting.
Now, suppose as an example we are going to build the view for Screen A. The top-most view function looks at the Current Screen flag and sees the user should be on Screen A, so calls out to Screen A's view function.
What arguments does the Screen A view function accept? Perhaps we pass the entire Widgets portion of the central data store, since we know it will contain some of those widgets. But the purpose of Screen A's function is only to layout the user interface, as identified in its settings of the Screens field. It doesn't need to know the inner workings of each widget's settings, only which widgets are selected and which widgets to build. But those widgets will not be able to build themselves if Screen A's view function doesn't also get lots of information about the widgets' settings so it can pass those on. And obviously, the widgets will need access to the GPS data, so despite that the Screen view function doesn't directly care about GPS data, we must send all of that raw data along as well, to a user interface toggle that is not actually performing any GPS analysis.
So we pass all the Screens data, the GPS data, and also the Widgets to the Screen A view function, even though it doesn't need most of it. Then that function calls out to individual view functions for widgets, which receive GPS data and the saved settings from the Widgets.
Let's say eventually we get to a widget that is "shareable", in that it allows the user to send it off to a third party. The widget must provide to the user a dropdown menu of all available Friends. Up to now, perhaps no other widget needed this, and certainly our UI layout functions didn't care about the user's friends. But, to build this one dropdown menu, we now need to pass the Friends data down through from the very top, and thus all the intermediate layout functions must also get this data. We are now passing nearly the entire model state through our app just so one menu far down the tree can build itself.
I read on the Elm-Discuss mailing list that one solution to this is to have a pre-processing step before your whole view tree starts to render. This function takes the model data and transforms it into a new format that lines up with your view tree. This is not an elegant (or likely performant solution) but it also suffers from the fact that you now have the structure of the view tree essentially mirrored in two places: the view functions theselves, and also a pre-processor function.
I've heard on Slack that a common idiom is to just pass model state throughout the app.
The problem of passing sections of a model (or a whole model) to a view tree that requires the data to line up that way appeared as a design problem in the first version of Om, a popular Clojurescript UI library from 2014. The idiomatic usage of Om was quite similar to the Elm Architecture in many ways. It has since been replaced by newer techniques in Clojurescript to address the above scenarios. My impression has been that using Elm would also find these stumbling blocks at a larger scale. Imagine adding one feature way down the line in your view tree, and that feature requires a part of the model data that no other part of that view tree needs. Now you are stringing along lots of different pieces of your central data through lots of functions that just pass them on -- in the end, it's easier to just pass whole model state.