Someone in the Elm slack channel threw out this idea of naming Msg
in the past tense, and not imperatively. I thought it was an interesting idea and I adopted the practice, just to try it out. I forgot who it was, I wish I could give them credit.
Anyway, the ramifications were more than I expected, and not simply the same Msg
with different names. What I started doing is naming Msg
as if they were saying "This happened". So where I would say "HandleUsernameField" I might instead say "UsernameFieldChanged" or instead of "Close" I would do "XClicked". What I didnt account for was that Msg
and functionality dont map one to one. So for example, if you have a Msg
named Navigate
, its going to be the one Msg
you use whenever you want to navigate. But if you are naming Msg
as paste-tense descriptions, then several different things could happen that could cause a navigation. Since many things should cause a navigation, naming Msg
in the past tense leads to lots of Msg
which do the same thing.
-- imperative
Navigate route ->
model => navigateTo route
-- descriptive
NavTabClicked route ->
model => navigateTo route
UrlChanged route ->
model => navigateTo route
SubmitClicked route ->
model => navigateTo route
This looks really redundant right? Its just the same functionality under three different Msg
. But there are some less obvious advantages. In all likelihood, a fully build app will have several pieces of functionality that will be merely similar and not totally identical. Having Msg
for every possible occurrence forces you to break down functionality into smaller pieces, and apply those pieces under the Msg
where they are necessary. Thats better than making individual Msg
that try and be flexible enough to do everything. Msg
that try and do everything end up as large blocks of code with lots of conditional logic which are hard to work with and end up super fragile. For example, the code above may very well evolve into..
NavTabClicked route ->
let
cmd =
[ navigateTo route
, saveSession model
]
|> Cmd.batch
in
model => cmd
UrlChanged location ->
model => navigateTo (Route.fromLocation location)
SubmitClicked ->
{ model | state = Pending } => navigateTo Home
They all use navigateTo
, but the functionality is a little different under each Msg
. This is a lot better than trying to force an all purpose Navigate
Msg
to handle all these cases.
Another thing happened, which is that I found myself writing lots of Msg
that didnt do anything. Like..
UserDecoderFailed err ->
model => Cmd.none
InvalidPortPayload ->
model => Cmd.none
LogOutSucceeded ->
model => Cmd.none
Lots of things happen in an app that just dont lead anywhere, and when I got into the mind set of writing Msg
per thing-that-can-happen, I found myself writing a lot of do-nothing Msg
. Maybe this is clutter, and to that extent I guess its bad, but if you look at these messages coming in (maybe in the debugger for instance) these Msg
names create a perfect record of what happened, even if I dont need my program to do anything as a consequence of these do-nothing Msg
. That perfect record of what happened could be really useful for debugging and discovering errors. Consider the alternative, where instead I just had a Msg
called NoOp
that I use everywhere I dont need functionality to occur. The record would show a huge list of NoOp
, which isnt informative of whats actually occuring. A list of do-nothing Msg
is actually quite descriptive of whats happening.