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.

Reply via email to