Greetings!
I originally posted this on
https://github.com/elm-lang/elm-compiler/issues/985 which, as Richard
Feldman pointed out, is probably not the best place to put it. I'll follow
Richard's suggestion & be a bit less abstract.
I'm building an application to help ship captains create routes. My
question is about the organization of the model of this application.
The application currently has one page containing three widgets:
1. A map showing the routes
2. A table showing the performances (eg. fuel consumption, duration...)
of each route
3. A profile widget showing the speed profile on the selected route
It is structured in three layers of decreasing size (and increasing
versatility):
1. The top layer, providing synchronization between the widgets and the
layout of the page
2. The widget layer (all widgets are independent)
3. The elementary widget layer (containing sliders, date pickers...) ie.
reusable components
The reusable components have their own model, which is independent from the
rest of the application: their model is initialized by their containing
widget.
The widgets, however, must share some information (eg. the list of routes)
but not all (for example, the profile widget couldn't care less which route
is hovered in the table widget, but the map widget does) because it
complicates refactoring (changing the model one widget impacts the others)
and makes it difficult to reuse widgets (eg. use the profile widget in a
context where I do not need/have the fuel consumption).
Now in Elm's architecture tutorial, there are two extreme cases:
- The same model is shared by all widgets
- Each widget has its own independent model
I find myself somewhere in between: part of the model is shared by all
widgets and is application-independent (eg. the waypoints of each route),
part of it is shared only by two widgets and mostly concerns the layout
(eg. route hovering) and part of it is not shared (and should not be) (eg.
the sliders' state).
At the top-level (Page):
type alias Model a =
{ a
| routes : List Route
, hovered : Maybe Int
, selected : Maybe Int
, map : Map.InternalModel
, table : Table.InternalModel
, profile : Profile.InternalModel
}
The Profile widget might use a part of this model:
type alias Model a =
{ a
| route : List Route
, selected : Maybe Int
, profile : InternalModel
}
while the Table widget uses another:
type alias Model a =
{ a
| route : List Route
, selected : Maybe Int
, hovered : Maybe Int
, table : InternalModel
}
Just like @rgrempel in https://github.com/elm-lang/elm-compiler/issues/985,
I want each module also provides its init function to initialize its part
of Page's model. With extensible records, I could simply do (eg. in Table):
init : a -> Model a
init foo =
{foo | table = initInternal}
and for Page I would have the very clean and composible chain:
init : Model
init =
{routes = []}
|> Table.init
|> Profile.init
So the extensible records were an easy way for me to build composable
applications. With the removal of this feature, the init function can no
longer be type-parametrized. This is really important so let me emphasize a
bit: *no extensible records means init must know the full record it
operates on*.
So I decided to use @rgrempel's strategy, but in his case where all parts
of his model were independent. The only workaround I found is to do the
following for Page (top-level):
type alias Model a =
{ a
| shared : Shared.Shared
, map : Map.InternalModel
, table : Table.InternalModel
, profile : Profile.InternalModel
}
init : Model
init =
{ shared = Shared.init
, map = Map.init
, table = Table.init
, profile = Profile.init
}
When you loose extensible records you have to put all shared parts in a
Shared record, which essentially means you know in advance how your widget
will be used. For instance, the data shared by the Profile and the Table
widgets is not the same as that used by the Profile and the Map widgets and
if I add another widget, chances are I'll have to modify the Shared record.
This makes me sad because it breaks separation of concern. With extensible
records, I could simply add the fields I need to Page's model & in the
specific widgets & they would simply be ignored by the other widgets.
Whenever I modify what is shared, I'm modifying the Shared record that all
widgets depend on &o if I decide to use eg. the Profile widget in another
application, it will quickly become unmanageable.
As previously stated, as soon as you define an init function, the record it
returns (or operates on) can no longer be type-parametrized (i.e.
extensible), which means that if init returns the shared part, Shared is no
longer extensible. If Shared is not extensible & you want to include Page
in a bigger application, Page's model will have to be completely
independent from the other widgets' model at the same level, ie. it will
not be able to share part of its model with the other widgets.
With extensible records, you could apply the same pattern to any number of
levels, but as soon as you put shared data in a shared field you're
basically stating once and for all what is shared by all possible widgets:
that information is only needed at the application level (at the Page
level), but it dribbles down to all widgets (which shouldn't care whether
they're being used in isolation or not).
Sorry to ramble on about this, but it's been a thorn in my side for a long
time now.
I would really appreciate any thoughts on this.
--
You received this message because you are subscribed to the Google Groups "Elm
Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
For more options, visit https://groups.google.com/d/optout.