I have been a great fan of the fact that Elm forces you to consider every possibility in case
expressions, and readily understand how much safer this makes it to “just change” code and know that the compiler will make sure you consider all the consequences of the change.
Until recently I happily accepted that the price for this is that sometimes one must write “unnecessary” code (e.g. sometimes you know a Maybe
will never be Nothing
), and have been doing my best to avoid _ ->
constructs wherever possible.
But recently, in trying to embrace the ideas from Richard Feldman’s talk about “making impossible states impossible”, I have found myself regularly using custom types rather than records for my Model
and I find this leads to me resorting more and more to _ ->
in my update
loop. I’m hoping someone can suggest a better design pattern to avoid this, or explain how they deal with it…
Let me illustrate the problem with a toy example. Say I have a Model
with four states:
type Model
= Init FormData
| Running Document Status
| PopupDialog PopupState Document Status
| FatalError String
where FormData
is a record containing String
fields I need the user provide to start my application, e.g. maybe an address to connect to in order to retrieve some data, a password, the name of the data etc. etc. Most of this will never be needed again once the application is initialised and those things that are needed will no longer simply be strings but instead become an integral part of the Document
which I actually get back and where the major work of the application takes place. Additionally I want to model some kind of dialog that might need to appear on top of this work (maybe when the user wants to commit changes back to the Document’s source, or if there is a conflict etc etc), and be able to tell the user when something unrecoverable has gone wrong.
The Msg
type for my update
loop would thus look something like:
type Msg
-- Init related messages
= AddressChanged String
| PasswordChanged String
| ResourceNameChanged String
| SubmitRequest
-- Init -> Running transition related messages
| DocumentReceived
| RequestError String
-- Running related messages
| DocumentEdited String
| VerifyDocument
| AddComment Position String
| EditComment Position String
| DeleteComment Position
| StatusChanged Status
| CommitChanges
-- PopupDialog related messages
| CancelPopup
| PopupFieldChanged String
| PopupTickBoxChecked Bool
| SubmitPopup
The problem now is how best to organise the update
loop. There only seem to be two choices.
Choice 1
update msg model =
case model of
Init formData ->
case msg of
-- Handle all Init and (Init -> Running transition) related messages
...
-- Explicitly ignore each and every other message, or else resort to "_ ->"
Running doc status ->
case msg of
-- Handle all Running related messages
...
-- Explicityly ignore each and every other message, or else resort to "_ ->"
PopupDialog state doc status ->
case msg of
-- Handle all PopupDialog related messages
...
-- Explicitly ignore each and every other message, or else resort to "_ ->"
FatalError err ->
-- This time we really can safely ignore everything!
( model, Cmd.none )
But the problem is as each state becomes more complex the more messages there are to explicitly ignore and eventually the temptation to just use _ ->
is overwhelming, but that of course makes the code fragile to changes.
I mulled over the idea of instead using _ -> FatalError "Incorrect message received"
and then removing it in production. But the problem is sometimes a non-relevant message may in fact occur and should be ignored (e.g. the user accidentally double clicks/double touches the submit button during the Init
process and the second copy of the event/message arrives when we have already transitioned to Running
).
Choice 2
update msg model =
case msg of
AddressChanged newVal ->
case model of
Init formData ->
-- return an updated FormData, possibly applying verification first
-- Explicitly ignore each and every other model pattern/subtype or resort to "_ ->"
PasswordChanged newVal ->
case model of
Init formData ->
-- return an updated FormData, possibly applying verification first
-- Explicitly ignore each and every other model pattern/subtype or resort to "_ ->"
...
This at least ensures that every message type is always handled, but even with only a few different Model
patterns/subtypes to match on requires a lot of writing code to ignore messages or resorting to _ ->
. Although it instinctively feels somehow less dangerous to use _ ->
when pattern matching on the Model
than on the Msg
.
Choice 3
Okay, so actually there is a third choice I can think of, which is to split all or some of the different Model
subtypes off into separate “pages” each with its own message type and update loop as in the standard SPA example. But this feels quite a big thing to do. I am finding that many of my applications fall in to the category where they don’t feel big enough to break into separate “pages” but are nevertheless large enough that it becomes very hard not to resort to liberal use of _ ->
in the update
loop.
–
So the question is: is there a fourth choice/design pattern that I’m missing? How do others deal with this when using a custom type for their Model
?
Thanks! And sorry for such a long post, but I couldn’t see how to explain it without a fair bit of code to show what I meant…