I used this technique for a while, but eventually gave up. The reason being that it is a hassle to work this way. Usually during development a state machine will grow and be adjusted quite a bit, I don’t always have something fully designed ahead of time. Its just a lot simpler to work with a state machine as (Model, Msg) tuple and convenience won the argument.
It can depend on your Model, but sometimes there is an obvious ‘partitioning’ of different Msgs between the different Model states, in which case a good way to minimise the negative effects of a catchall can be something like this:
type Model
= FirstState StartingData
| SecondState FollowupData
| ThirdState FinalData
type Msg
= First FirstStateMsg
| Second SecondStateMsg
| Third ThirdStateMsg
type FirstStateMsg
= ...
type SecondStateMsg
= ...
type ThirdStateMsg
= ...
update : Msg -> Model -> Model
update anyMsg model =
case ( anyMsg, model ) of
( First msg, FirstState data ) ->
updateFirst msg data
( Second msg, SecondState data ) ->
updateSecond msg data
( Third msg, ThirdState data ) ->
updateThird msg data
_ ->
-- Stale message, ignore
model
updateFirst : FirstStateMsg -> StartingData -> Model
updateFirst msg data =
case msg of
...
-- NO CATCHALL USED HERE
-- updateSecond, and updateThird similarly
This way you can ignore ‘stale’ Msgs with a catchall, but the compiler still has your back every time you add a new message (and makes sure you add code to deal with it) because there are no catchalls used in updateFirst, updateSecond etc.
Some problems can’t be modeled in a way that eliminates all impossible states. Others technically can, but the representation is so clunky as to be unusable. This article explores the limits of modeling to avoid impossible states and when to blend it with other strategies.