Elm-Action - Update your Model using a State Machine

As your Elm project gets bigger, you start having multiple pages, all with their model/update/view. Setting up your main update function to pass the Msg to the correct page and then convert the response back to a (Model,Cmd Msg) is quite tricky and from personal experience I can tell you: if you do it wrong, its very hard to read and even to refactor. Today I present to you to an out-of-the-box solution that even prevents you from writing wrong or dead code.

https://package.elm-lang.org/packages/Orasund/elm-action/latest/

Example

Here is a quick example of it in action:

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case (msg,model) of
        (GuestSpecific guestMsg,Guest) ->
            updateGuest guestMsg
                |> Action.config
                |> Action.withTransition initUser User never
                |> Action.withUpdate (always Guest) never
                |> Action.apply
        (UserSpecific userMsg,User userModel) ->
            updateUser userMsg
                |> Action.config
                |> Action.withUpdate User UserSpecific
                |> Action.withExit (Guest,Cmd.none)
                |> Action.apply

An update can return one of three actions: transitioning, updating or exiting. In the example we see that Guest can update (if the logging failed) or transition into User (if the logging was successful). A User can update or exit back to a Guest(by logging out).
The allowed actions are specified in the type. This way the compiler will call you out if you use a wrong action. The same happens if your config pipeline is wrong.

Under the hood, this is actually just a state machine.

Alternative Solutions

I’ve seen multiple different approaches for the same problem and it might be that my solution isn’t suitable for your needs.

  • In the Elm Spa Example, a helper function updateWith is used for the wiring. Transitions are all defined in the function changeRouteTo. Personally I like to model my app as a state machine. But if you don’t, then the approach in the SPA example will be better for you.
  • the-sett/elm-state-machines has the same idea as I do, but implements it using phantom types. Instead of phantom types, I use the config pipeline to specify what actions are allowed.
  • turboMaCk/glue introduces the concept of subModules. This makes a lot of sense for reusable views. If you only use reusable views, then use that package instead.
4 Likes

This looks real interesting, just not had time to try it out yet. I wrote the-sett/elm-state-machine.

In my attempt, the state machine is defined seperately, then used in the update. What is exciting about your version of state machines is that the state machine and its use in the update occur in the same place in the code. So I can look at your example code and read off or maintain the state machine right where it is used. That seems simpler and very appealing.

1 Like

Exactly. It was very interesting to write the package, as I didn’t believe that it was actually possible. I clearly overused the Never type a lot and I’m quite happy that in the final version the user does not necessarily need to know that it exists. All the trickery is hidden in the Action.config.

That said, the compiler errors are far from optimal. Right now the compiler says something like:

Action
    Model
    Never
    Int
    Never
is the wrong type

I myself find it difficult to remember what the different arguments mean. Im looking at a way of making it more readable.
Maybe by using a generic alias type?

type alias MsgOfType a =
    a

Now the error message would look something like this:

Action
    (ModelOfType Model)
    (MsgOfType Never)
    (TransitionOfType Int)
    (ExitOfType Never)
is the wrong type

Im not quite sure though if that is a good way of doing things.

1 Like

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