A few times, I’ve found myself with a model that needs some invariants to hold, and I’ve stumbled on a pattern that seems to make that work better, but I can’t decide if it’s a good pattern or a bad one. I’d be interested in your feedback.
I think it’s easiest to explain the problem by example. Imagine you’ve got this model:
type alias Model =
{ items : List Item
, sortOrder : SortOrder
}
type SortOrder = ById | ByPrice
and messages like:
type Msg
= ReplaceItems (List Item)
| AddItems (List Item)
| SetSortOrder SortOrder
In your view, you’d like to display items
from your model in sorted order according to the configured ordering.
You could easily sort the list inside your view
function, but that seems like a bad idea since sorting a big items
list on every render could get expensive.
Probably a better approach would be to have your update
function maintain the invariant that items
is always sorted according to sortOrder
. It’s not obvious how you could make a representation that couldn’t represent items that weren’t sorted according to the given order, so the “make impossible states impossible” approach doesn’t seem viable. Using the model we have, the code to do this directly looks like:
update : Msg -> Model -> Model -- (no commands for simplicity)
update msg model =
case msg of
ReplaceItems items ->
{ model | items = sortBy model.sortOrder items }
AddItems items ->
{ model | items = sortBy model.sortOrder (List.concat items model.items) }
SetSortOrder sortOrder ->
{ items = sortBy sortOrder model.items
, sortOrder = sortOrder
}
This code is problematic to maintain: You have to remember to maintain the invariant everywhere that could change items
or sortOrder
! It would be easy to add a new case someday and forget that you needed to do this, which would end up introducing a bug. (And if you’ve forgotten the invariant – or if it’s your coworker, who never knew about it in the first place, who writes the new code – it might be hard to figure out what’s going wrong.)
I’ve been in situations like this a couple times, and both times I’ve ended up with the same solution: allow the main update
function to break the invariant, and then pass the result of update
through a post-processing function that repairs it. In this example it would look like:
updateBreakingInvariants : Msg -> Model -> Model
updateBreakingInvariants msg model =
case msg of
ReplaceItems items ->
{ model | items = items }
AddItems items ->
{ model | items = List.concat items model.items }
SetSortOrder sortOrder ->
{ model | sortOrder = sortOrder }
repairInvariants : Model -> Model
repairInvariants model =
{ model | items = sortBy model.sortOrder model.items }
update : Msg -> Model -> Model
update msg model =
updateBreakingInvariants msg model
|> repairInvariants
This has the advantage that you only have to state the invariant one time, and you don’t have to remember it every time you add or edit your update
cases. The downside is that you pay the cost of repairing your invariant whether the update broke it or not.
I’ve used this pattern a couple times in different contexts, but I’m not sure whether it’s a great idea or a terrible one. People who have run into similar problems, have you found better solutions to the problem? Does this have some obvious issue I haven’t noticed? Comments welcome!