Minimising the use of _->

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…

1 Like

I would like to know what is a better pattern for this too.

I have seen something like:

case (model, msg) of
        (Init formData, AddressChanged newVal) -> 

This allows to have one failing _ -> at the end. But we have avoided this because it makes the Elm 0.18 compiler quite slow. I’m also not sure if this is clearer at all.

We use pattern #2 from above quite a lot in our app.

We avoid reusing messages when we have this pattern, so we have something like:

type Msg
    = Stage1_DoSomething
   | Stage2_DoSomethingElse

Because of this we only have two branches for a message. The one with the correct model and the one with any other model, in this case we use _->.

I think this depends a lot on what exactly you are modelling, so the answers will be better the more specific your question is.

For this specific example you mention, another thing you can do, if it fits your design goals and current situation, is to create custom types for the messages that align with the constructors of the model. Something like this:

    type Model
        = Init FormData
        | Running Document Status
        | PopupDialog PopupState Document Status
        | FatalError String


    type Msg
        = InitMsg InitMsg
        | RunningMsg RunningMsg
        | PopupDialogMsg PopupDialogMsg


    type InitMsg
        = AddressChanged String
        | PasswordChanged String
        | ResourceNameChanged String
        | SubmitRequest
        | DocumentReceived
        | RequestError String


    type RunningMsg
        = DocumentEdited String
        | VerifyDocument
        | AddComment Position String
        | EditComment Position String
        | DeleteComment Position
        | StatusChanged Status
        | CommitChanges


    type PopupDialogMsg
        = CancelPopup
        | PopupFieldChanged String
        | PopupTickBoxChecked Bool
        | SubmitPopup


    update msg model =
        case ( model, msg ) of
            ( Init formData, InitMsg initMsg ) ->
                case initMsg of
                    AddressChanged str ->
                        Debug.todo "Do things"

                    PasswordChanged str ->
                        Debug.todo "Do things"

                    ResourceNameChanged str ->
                        Debug.todo "Do things"

                    SubmitRequest ->
                        Debug.todo "Do things"

                    DocumentReceived ->
                        Debug.todo "Do things"

                    RequestError str ->
                        Debug.todo "Do things"

            ( Running doc status, RunningMsg runningMsg ) ->
                Debug.todo "Match only on RunningMsg states"

            ( PopupDialog popupState document status, PopupDialogMsg popupDialogMsg ) ->
                Debug.todo "Match only on PopupDialogMsg states"

            ( _, _ ) ->
                Debug.todo
                    ("We don't have messages right now that are not namespaced or global to all model states"
                        ++ "And FatalError doesn't seem to use any messages to do anything so ignore the rest of options"
                        ++ "So ignore anything that comes here."
                    )

By doing this, I think the code easily shows a few things:

  • FatalError doesn’t have any interactivity, as it has no related messages
  • Each stage (constructor) of the model type have a set of isolated messages it interacts with, so it is a fairly well isolated state machine
  • If you add new steps to a stage, the pattern matching will help you add the new code
  • We default to ignoring messages from other stages as the preferred strategy. But we could do something different (error reporting) or add specific clauses to the main case to catch specific messages/state pairs and report errors or do state transitions. If this happened though, I’d give a second look to the interactions to see how this could be avoided.

If this starts getting very big, I’d make some functions for the different sub-case functionality, like:

updateInit : InitMsg -> FormData -> (Model, Cmd Msg)

updateRunning : RunningMsg -> Document -> Status -> (Model, Cmd Msg)

Those functions could make the state transitions and transition to a different model constructor stage. Playing with types here also brings a lot of information, for example, if updateInit doesn’t return a Cmd Msg then you know it doesn’t produce side effects, or if instead of returning a Model it returned a FormData then you would know that function would never transition model to a different stage.

Like I said at the beginning, the modelling and design is very specific to the particular use case, specially because you can use the type system and language tools to get the biggest benefits for your particular use cases.

3 Likes

Thanks! I really like the idea of creating update functions that are more specific to both message and model subtype/pattern.

I definitely think splitting messages into subtypes will help with a lot of my projects.

Unfortunately I do have one thing I’m working on at the moment that doesn’t easily fit into that category: I have NetworkMsg messages that reflect loss of connections, receipt of data, etc. that need to be handled by multiple, but not all, different model states. I’ll have to have another look at that project to see if there’s a way I can split this up to make things easier along the same lines…

If not I’ll maybe post the specifics to see if there’s something I’ve missed.

Thanks again :slight_smile:

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.