Restructuring codebase

Days like these I’m really grateful that Elm compiler catches lots of type mistakes. I have been structuring the client side code to separate different states of the program (user has not logged in, user has logged in but doesn’t have an avatar and user has logged in and has an avatar) more clearly. As usual, I failed to see all the little details I need to take into account, but Elm compiler was nice enough to point them out to me.

Originally I wanted something along the lines of:

type Model
    = NotLoggedIn LoginModel
    | UserWithoutAvatar AvatarSelectionModel
    | UserWithAvatar FullModel

But it turns out, that there’s lots of data that is common with all the states of the program, so I ended up with:

type alias Model =
    { key : Key
    , url : Url
    , currentTime : WebData StarDate
    , errors : List ErrorMessage
    , navbarState : Navbar.State
    , messageToast : MessageToast Msg
    , subModel : SubModel
    }


type SubModel
    = NotLoggedIn LoginModel
    | UserWithoutAvatar AvatarSelectionModel
    | UserWithAvatar FullModel

And this in turn lead me to write some helper functions. I’m using elm-accessors to manipulate my model. Previously, when client received an error response to a query about planet details, following piece of cod did the handling:

update : PlanetRMsg -> FullModel -> ( FullModel, Cmd Msg )
update msg model =
    case msg of
        PlanetDetailsReceived (Failure err) ->
            ( set (planetRA << planetA) (Failure err) model
                |> over errorsA
                        (\errors -> error err "Failed to load planet details")
            , Cmd.none
            )
        ...

Now that errors are in Model and not in FullModel, this doesn’t work anymore. So update function needs to either return triple (FullModel, List Error, Cmd Msg) or operate on Model and return (Model, Cmd Msg). I opted for the latter, which meant that I needed a way to update data that might or might not be there. In Haskell lens library, there is handy abstraction prism that would be perfect match for this. I tried to implement something similar on top of elm-accessors, but failed. So I had to choose the next course of action, which was to write a specialised set function that encapsulates this logic:

setFullModel : (Relation sub sub sub -> Relation FullModel sub wrap)
    -> sub -> Model -> Model
setFullModel accessor value model =
    case model.subModel of
        UserWithAvatar m ->
            let
                nm =
                    set accessor value m
                        |> UserWithAvatar
            in
            set subModelA nm model

        _ ->
            model

setFullModel is used exactly like set, with some tweaks. It returns a new, modified Model only if model.subModel is UserWithAvatar (meaning user has logged in and has an avatar). In all other cases the original Model is returned. This allowed me to restructure earlier code to read:

update : PlanetRMsg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        PlanetDetailsReceived (Failure err) ->
            ( setFullModel (planetRA << planetA) (Failure err) model
                |> over errorsA
                        (\errors -> error err "Failed to load planet details")
            , Cmd.none
            )
        ...

Note how I can chain setFullModel and over together. What I really wanted, but failed to achieve was:

update : PlanetRMsg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        PlanetDetailsReceived (Failure err) ->
            ( set (subModelA << tryFullModel << planetRA << planetA)
                  (Failure err) model
                |> over errorsA
                        (\errors -> error err "Failed to load planet details")
            , Cmd.none
            )
        ...

tryFullModel would have been an accessors that works in the similar way as try in elm-accessors that focuses on Just side of Maybe.

Maybe I’ll revisit this problem in the future.