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.