thoughts on modularity, real re-use, app-wide coordination
Opened this issue · 4 comments
Hi Elm community,
This is not an issue, just a discussion I would like to have on building modular, well-defined applications under the elm architecture. I will try to keep it brief, which will require a lot of hand-waving.
The most important pieces of any application architecture I have implemented always seem to turn out the same:
- modularity / re-use with good separation of concerns
- the ability to clearly and predictably communicate between components
- the ability to concisely and predictably coordinate multiple components to perform major context switches / transitions.
These three pieces enormously impact the refactor-ability of the codebase, the velocity of developers in that codebase, and the lifespan/maintainability of the entire application.
Modularity / Re-use
Ideally an entire application is built from the bottom up from small, loosely coupled, reusable components. If these components can be completely unaware of their place in an application, and only serve a small, defined purpose with a minimal api surface, you are on the right track to a highly maintainable and refactor-able code base.
Clear and predictable communication
If each component is modular, you can build up a tree hierarchy of components where communication between depths or leaves or branches happens only across a small interface at defined intersections. No matter the form of communication - be it a flux architecture, a messaging bus, or pub/sub, this "bounded" communication also keeps you on the right track to a highly maintainable and refactor-able code base.
The ability to coordinate context switches
Most apps have major contextual chunks that form concrete or logical boundaries in which to partition your components. Sometimes this is as simple as main navigation tabs, and sometimes it is more nuanced (consider following a deep link to a different part of your application). If your architecture does not offer a way to reliably and conveniently swap between these contexts, you are going to end up with a lot of fragile code that has to be hand-held through state re-hydration, serialization, branching within contexts, and all sorts of other tedious and error-prone bandaids.
Where does elm fit in?
I have been fooling around with the elm architecture for a little bit, and have tried to familiarize myself with its strengths and weaknesses. It seems relatively similar to the redux pattern (immutable, explicit top->bottom action flow, predictability) with perhaps the major difference being that truly reusable components are much simpler.
Modularity
Using Signals and forwardTo, parents provide their reusable children the information necessary to "just work" so that children can be completely unaware of the rest of the application. There are plenty of (mostly) clear examples where this can be extended to allow lists of lists of entirely dynamic reusable components (the "Model with a context" example is still pretty unclear to me).
One issue I bumped into almost immediately using this pattern is a similar but slightly more advanced use case: when a parent does not merely want to forward its child's actions, but is interested in a subset of those actions.
For example, consider a simple chat interface with a MessageBox , a Post, and a Chat component to hold everything together. What I want to be able to do is pipe InputChanged into the MessageBox without needing to know the details, but when MessageSent appears, capture some details about that action inside of the parent. A rough sketch would be something like...
type Action
= MessageBoxAction MessageBox.Action
update : Action -> Model -> Model
update act model =
case act of
MessageBoxAction msg ->
case msg of
-- for a particular action, do something before forwarding it on to the child
MessageBox.Action.MessageSent ->
let
updatedMessage = MessageBox.update msg model.currentMessage
newPost = Post.init msg model.posts
in
{ model |
posts = model.posts :: [ newPost ]
, currentMessage = updatedMessage
}
-- for any unspecified actions, just forward to the child
_ ->
{ model |
currentMessage = MessageBox.update nested model.currentMessage
}This won't compile. I can think of a few other ways to achieve the same end, but none of them are a good idea because they either burden the reusable component with unnecessary requirements or end up with circular and difficult-to-trace "on update do another update" patterns.
I feel like if a cleaner general pattern can be reached (and it might be simple and I'm just missing it), then truly reusable components are actually possible in elm.
Communication
This is pretty inherit in elm. Everything is explicit, and the communication lines are well drawn down a component hierarchy. The biggest challenge I believe will be maintaining a small API surface across components. Sometimes the temptation is strong to update on actions that probably aren't any of your component's business. This eventually carves out an application where changing one action name, or removing a single component can break everything in that hierarchy subtree.
root
a b
c d e f g
h i j k l
"Update H" needs to be piped from a -> c -> h. In a naive implementation, changing the action to "Update h" requires also changing a and c. I think elm avoids this through the use of a generic "On some child action just pipe the message through and update the model".
Context
I have no idea how elm might handle something like this. I believe the excellent work done over at redux-saga handles context switching in one of the most elegant ways I have seen. Because redux, like the elm architecture, is basically an event-sourced architecture, the concept of sagas "just works". When SOME_TRANSITIONING_EVENT arrives, you can utilize the api of components to predictably transition the application step by step.
The problem with sagas, though, is they are by-nature imperative and not functional. It would be great to hear some thoughts on how the elm architecture handles this case.
As you have seen with MessageBoxAction msg an action can hold a value. Why not make MessageBox.Action.MessageSent hold the send message as a string? But I myself am very new to Elm too, so feel free to submit better solutions.
Hi, i'm trying to build websites using elm.
As far as modularity is concerned, Why not allow simple communications between modules, like subscriptions do?
This would allow one to build apps bottom up.
For instance, i want an input component, and an "accumulator of inputs" component
When enter is hit, the whole input is piped to listening components ...
The Top down approach will have the "supervisor" (Top most component) have to store a lot of information ...
If the hierrachy is deeply nested, then it might get tedious to maintain "sense" in that top most component ...
What you guys think?
Expect more discussion.
Here's the beginning of advice on this kind of stuff: http://guide.elm-lang.org/reuse/
We will have more resources coming in the next few months, but that is the gist. If you are thinking about components, if you are thinking about parents and children and communication, something has already gone wrong.
Also, this community uses forums like elm-discuss and the Elm slack for discussions like this. Issues are for tracking concrete problems and work, not for discussions.