How to prevent invalid state with Maybe types in the model?

Let’s assume we’ve got the following application, which requires to know the timezone of the user, by relying on the Time package. It would need to be something like this, I think:

type alias Model = { timezone : Maybe Time.Zone }
type Msg = GetTimezone Time.Zone

init : Flags -> Url -> Browser.Navigation.Key -> (Model, Cmd Msg)
init flags url key =
  ({timezone = Nothing}, [Task.perform GetTimezone Time.here])

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    GetTimezone timezone =
      ( { model | timezone = Just timezone}, Cmd.none )

and the application would start with timezone = Nothing and then once the Task returns it will be Just <whatever_timezone>.

However, this create a problem with dealing with invalid states:

  1. We’ll need to check if the timezone is there or if it’s Nothing every time we want to use it. This quickly created situations where it wouldn’t make sense to verify that (invalid state). For example, if we’re doing a tick subscription every second it doesn’t really make sense to check every second if the timezone is Nothing or Just timezone.

Is there a way to delay the application somehow, until these initial messages which are necessary for the program’s behaviour to return? Ideally it wouldn’t be possible to have the Nothing values in those model fields simply because the application shouldn’t operate before it knows those values.

I’m going to also leave an Ellie here which I think better illustrates this problem by relying on the Loading... text as a workaround until the messages arrive.

Thanks!

The situation you have going on here can be put into 1 of 2 categories.

  1. There is some initialization that must happen outside of the elm app and before the elm app can be started. In that case, get the timezone and initial time in JS land and pass them into the app via Flags. Then your model won’t need the Maybes and your app wont start if there is no initial timezone or time.

  2. Your app model has two main states, Initialized and not Initialized, and you want to handle the initialization in elm.

The way I have typically handled this is to separate my Model and Msgs into Loading and Ready versions and slap all the extra harness around my init/update/view/subscriptions functions. In an app that I’m writing I use this method to distinguish between a “logged in user” and a “logged out user”. This creates sort of 2 different applications in one, the first existing only to log the user into the 2nd.

I’ve updated your ellie example to show this with the timezone / current time. There are more helpers you could write to make the code look a little better here and there but I think bare-bones it’s easier to follow.

https://ellie-app.com/3xfZyjXf7VZa1

Things to notice:

  • The model has no Maybes
  • The model is a Maybe but could be a union of many different records
  • There is no way to handle an incorrect model/message combination
  • The only case catch-all simply handles 2 potentially invalid cases, that you could match on if you wanted
1 Like

I would have done something very similar to @itsgreggreg; build a state machine with 2 states, and different models for each state. I don’t think I would have bothered to split the Msgs down into two separate sub-types though - perhaps that would appeal if there were >1 message relevant to each of the 2 states, or a larger application.

What I generally do is something like this:

type Model 
  = Initializing
  | Ready { field : FieldType, ... } -- No Maybes as all initialized

type Msg
  = Loaded ... -- Loaded some data, say, to complete the init
  | ClickedMouse ... -- User interaction in the ready state
  | AndSoOn ...

update msg model = 
    case (msg, model) of
      (Loaded data, Initializing) -> ...
      (ClickedMouse event, Ready ready) -> ...
      (AndSoOn stuff, Ready ready) -> ...
      (_, _) -> (model, Cmd.none) -- Do a no-op if the state/event combination is not allowed.

So I make the model a state machine, but just keep the messages flat. States in the machine are paired with their possible events in the update function, and disallowed combinations are ignored. Note, disallowed combinations can occur in some situations due to the asynchronous nature of event processing, if events trigger fast enough - so having a state machine like this to protect against that is usually a good idea for that reason too.

Which reminds me, for a talk I am giving I need to make a state machine example for a coke machine, to show how someone pressing the button to vend a coke at the same time as pressing the button to return their money, can be prevented by using a state machine to guard against events firing in the wrong state. Except I live in Scotland, so it will be an Irn Bru machine, of course.

4 Likes

@itsgreggreg and @rupert thank you for your input. I ended up experimenting with Rupert’s approach (I prefer the union type for the Model instead of a Maybe), in a application much larger than the example I had shown, and it seems to have worked perfectly.

This seems like the correct way to look at the model, as a state-machine. I wonder if any more states other than Initializing | Ready could be useful? Would ever something like Exiting be useful and/or possible? Like, if the user quits the tab then send an HTTP request, or save the state somehow :thinking:

Yes, more states than init and ready are usually useful. For example, in SVG application I quite often want to know the window size, so the state machine might go Sizing -> Loading -> Ready.
Or I have an application where you can zoom into a diagram, and there is a transisiton from Ready -> ZoomedIn -> Ready for that. And so on…

State machines are a great way to model a UI, and also a modelling activity that translates very neatly into code in the case of Elm. When designing a new UI I tend to start with pen and paper sketches, iterate the design a few times that way until I think I have it right, and then go to code.

2 Likes

Another state could be an Error state. Nowadays my top model is a Result Error Model, where the Error represents some kind of initialization failure, such as key information not being in the flags.

type alias Model =
    { timeZone : Time.Zone }

type Error
    = TimeZoneMissingFromFlags

main : Program Decode.Value Msg (Result Error Model)
2 Likes

@Chadtech that seems a good usage of the state machine too! Should also be useful for bad payloads being received.

The one tweak I would make to this pattern is that I find it useful to log any unhandled messages since the (_, _) branch in the case statement defeats completeness checking. This can also be a reason to split the message space since the the top level message is just ToInitializing InitMsg | ToReady ReadyMsg assuming there are no messages that apply to both. It’s still useful to log unhandled messages but now they are not due to completeness issues but due to unexpected async messages which are presumed non-fatal but can be potentially interesting when something doesn’t seem to be working right.

Mark

1 Like

I like to think of my model a a state in a state machine too!

However, in a normal state machine, you get to define which state transitions are valid. But in Elm, the transitions are the Msgs, and so in your update you’re effectively forced to account for every possible transition out of every possible state – even if you know that some of those transitions can never happen out of some of those states.

If anyone’s figured out a way to represent that notion of impossible transitions (I.e. this particular kind of Msg can never happen to this particular kind of Model), I’d be super interested to hear about it!

To me, the fact that transitions have an initial state perfectly matches the fact that you can pattern match a pair ( msg, state ) in your update. Any wrong combination is simply discarded as not a valid transition. So I like to do as @rupert explained also.

I think since messages are async, you cannot prevent wrong combinations from happening.

Precisely.

The view is only updated max once per animation frame. That means there is always going to be a window of time, when events can fire against a stale version of the model - so wrong event/model combinations are always going to be possible. See this Ellie for an example:

https://ellie-app.com/yVC9wPk33xa1

Best to use a simple state machine to tame the asynchronous real-world that a user interface interacts with.

Okay, yeah – interesting, thanks! I’ll ponder … :slight_smile:

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