Removing impossible states in sign in/registration model

Hi all!

I’m modelling a sign in / registration form, where the form has two tabs to switch between sign in and registration. Both forms ask for email/password combo. Each tab has a button that changes label depending on the selected tab: “Sign In” or “Register”.

I found I had impossible states in my update function. Here’s the story of how I removed the impossible states. I’d love for others to tell me if I’ve used a correct solution, or if there’s an even better solution.

The code before I found a solution was:

type alias Model =
    { formState : FormState
    , authenticationState : AuthenticationState
    }


type Msg
    = SetEmail String
    | SetPassword String
    | ChangeFormState FormState -- switch between sign in / register
    | RunSignIn -- launch the sign in XHR
    | RunRegister -- launch the register XHR
    | SignInResult (Result Http.Error SignInResponse)
    | RegisterResult (Result Http.Error RegistrationResponse)

type AuthenticationState
    = Unknown -- initial state
    | InProgress -- during the XHR
    | Failed -- bad username/password
    | Authenticated JwtToken -- success!


type FormState
    = FillingRegistrationForm RegistrationRequest
    | FillingSignInForm SignInRequest

Given the above, I didn’t have much choice in update to do something similar to this:

 RunSignIn ->
     case model.formState of
         FillingSignInForm req ->
             ( { model | authenticationState = InProgress }
             , sign_in req )

         otherwise ->
             ( model, Cmd.none )

That otherwise was a strong red flag that told me there was an impossible state here: it was possible for the model to have the sign in form, but to receive a message that said we’re going to register.

I found a very simple solution: I changed the RunSignIn and RunRegister messages to accept their corresponding requests:

type Msg
    = -- ...
    | RunSignIn SignInRequest
    | RunRegister RegistrationRequest

This change provided me with two benefits that I could identify:

  1. I removed the impossible state and,
  2. The code to launch the XHRs is now simpler.
RunSignIn req ->
    ( { model | authenticationState = InProgress }
    , sign_in req )

RunRegister req ->
    ( { model | authenticationState = InProgress }
    , register req )

My original implementation was based on this model instead:

type alias Model =
    { email : Maybe String
    , password : Maybe String
    , authenticationState : AuthenticationState
    }

type AuthenticationState
    = Authenticated JwtToken -- successfully registered/signed in
    | Anonymous -- initial state
    | Authenticating -- during the XHR
    | AuthenticationFailure -- bad email/password

When everything was a single state, I had this different impossible state instead:

RunSignIn ->
    case ( model.email, model.password ) of
        ( Just email, Just pass ) ->
            ( { model | authenticationState = Authenticating }
            , sign_in (AuthenticationRequest email pass)
            )

        otherwise ->
            ( model, Cmd.none )

That being said, the code was almost completely rewritten, but I did baby refactorings until the new model and messages emerged. I had a rough idea of where I was going, and leaned on Git & Elm to save and guide me throughout the process.

Anyway, just another experience report from me. As I said above, if you have a different solution, or know of one, I’d like to know about it. Now that I’ve implemented a solution, I’m ready to read other solutions. I’ll admit I haven’t done any research, in part because I like to learn by getting my hands dirty.

Take care!
François

I think what you’ve in this specific situation is perfectly reasonable! :100: In fact, it may be better than previous for an additional reason: since you’re constructing the request based on the values that are currently rendered, you’re always going to be making exactly the request that the user is expecting. This eliminates potential message ordering issues with, say, subscriptions or returning tasks.

A sketch of another solution, which I’ve used with acceptable results:

registrationRequest : Model -> Maybe RegistrationRequest
registrationRequest model =
    -- the code in the update branch from your "before" example,
    -- but returning a `Nothing` in the `otherwise` case.

then you only have to match on the Maybe in update, which gets rid of the otherwise while still having something you can say semantically “I should get any valid RegistrationRequest from this function.”

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