How do you organize `update` with custom type Models?

@avh4 aren’t your way of modeling also introduce impossible state?

Yes, but I believe there’s a tension between strict modeling and simplicity. At NoRedInk, over time we’ve run into a lot of situations where precise modeling of the “possible states” we’re using now has turned out not to be the best choice for maintainability of the code. Specifically, “making impossible states impossible” can introduce coupling between fields that don’t need to be coupled, and the code can be simpler and more robust by allowing “impossible” states in many circumstances.

As an example, say there’s a page that loads and displays a list of items, and then an item can be selected causing details of that item to be loaded and displayed. If we are avoiding “impossible” states, we might use a model like this:

type Model
    = LoadingItems
    | ItemsLoaded
        { currentItem : Maybe (ItemSummary, ItemDetails)
        , previousItems : List ItemSummary
        , nextItems : List ItemSummary
        }
    | ItemsError String

type ItemDetails =
    = LoadingItemDetails
    | ItemDetailsLoaded
        { address : String
        , ...
        }
    | ItemDetailsError String

We’ve avoided impossible states–most notably, it’s not possible to have an ItemDetails if the list of Items isn’t loaded.

But it’s common in practice for UI designs to change. Say there’s a new design where the page is now more of a single-page app and it’s possible to directly link in to a specific item detail page without needing to show the list of items.

To implement this, we’d have to make the following refactor:

type Model
    = ItemsView Items
    | DetailsView (Maybe Items) ItemDetails

type Items
    = LoadingItems
    | ItemsLoaded (List ItemSummary)
    | ItemsError String

type ItemDetails =
    = LoadingItemDetails
    | ItemDetailsLoaded
        { address : String
        , ...
        }
    | ItemDetailsError String

I think a big issue with this kind of approach is that it makes the structure of your program overly tied to the specific UI design.

A better Model knowing that a UI redesign like that might happen would be:

type Model
    { items : WebData (List ItemSummary)
    , itemDetails : WebData ItemDetails
    , view : View
    }

type View
    = AllItems
    | ItemDetail ItemId

There are a lot of “impossible” states here: you can have item details loaded when the items are not loaded (only impossible in the first UI design); you can have item details loading when you’re not viewing item details; you can have item details loaded for a different item than the one you’re viewing; you can have “NotAsked” for the web data values in situations when you shouldn’t.

But this same model would work for both UI designs. (The refactoring above in the “avoid impossible states” approach is a pretty tedious one to do, so I think flexibility here has a lot of value.) This model is also less complicated to read and understand.

(I would say the most serious “impossible” state here is the one where “you can have item details loaded for a different item than the one you’re viewing”, but this can be addressed by a few different approaches that are simpler than the full-blown “avoid impossible states” approach.)

I do think there are times when strictly modeling to avoid impossible states is valuable, but it needs to be considered with its trade-offs against simpler modeling. Specifically, (as has happened often at NoRedInk in the past couple years) there are often states that are coincidentally impossible, but if there’s not technical reason that making them impossible improves your code, then it’s often a better choices for understandability and future extensibility to allow those states, especially in cases where allowing them makes your code simpler and more resilient to “safe” errors.

5 Likes

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