A road to state machines

Me 

  • Margarita, @rita_krutikova 
  • 🇺🇦 Ukraine  → Sweden, Gothenburg 🇸🇪 🌧
  • React + Typescript, Reason,
  • GraphQL + Apollo
  • F#, C#
  • Functional programming, F#, ReasonML
  • dev.to/margaretkrutikova

👩‍💻

🏠

Before we start

  • Assumes some basic knowledge of functional concepts
    • variants (DU)
    • pattern matching
    • pure functions/immutability
  • Examples in ReasonML
  • Beginner intro to state machines

Agenda

  • A few words about state machine
  • Pack your bag, we are driving out
  • State machine recap
  • Benefits 
  • Recognising state machines

Machine?

  • mathematical concept
  • design pattern in software development
  • domain modelling

    • ​represent data and behaviour in code

A road to state machine #1

type user = {
  isLoggedIn: bool,
};
type userData = {
  userName: string,
  email: string,
};
type user = {
  isLoggedIn: bool,
  userData: option(userData),
};
type user = {
  isLoggedIn: bool,
  userData: option(userData),
  tempUserName: option(string),
};

A road to state machine #2

let setTempUserName = (tempUserName, user) => 
  {...user, tempUserName: Some(tempUserName)};
let logIn = (userData, user) => {
  ...user, 
  isLoggedIn: true, 
  userData: Some(userData),
};
let logOut = user => {...user, isLoggedIn: false};
let postQuestion = (question, user) => {
  let authorName =
    switch (user.tempUserName, user.userData) {
    | (Some(tempName), _) => tempName
    | (_, Some(userData)) => userData.userName
    | _ => failwith("You can't post questions!")
    };

  post(question, authorName)
};

A road to state machine #3

A road to state machine #3

let setTempUserName = (tempUserName, user) => 
  {...user, tempUserName: Some(tempUserName)};

Is not allowed when logged-in! 🐞

#1 What, I just set temporary name while being logged-in! Now I am stuck with it! 😠

A road to state machine #3

let logIn = (userData, user) => {
  ...user, isLoggedIn: true, userData: Some(userData),
};

Temporary name is still there! 🐞

#2 Wait, I logged in, why does it show my temporary name? 🤨

A road to state machine #3

How about resetting userData? 🐞

let logOut = user => {...user, isLoggedIn: false};

#3 Haha, I logged out and could still post with my user name! 😆

What went wrong?

A road to state machine #4

type user = {
  isLoggedIn: bool,
  userData: option(userData),
  tempUserName: option(string),
};
  • Many invalid combinations
  • User can only be anonymous OR temporary OR logged in

One of the states at a time!

What went wrong?

A road to state machine #4

  • States are implicit
  • Update properties that belong to one state?
let logOut = user => {...user, isLoggedIn: false};
  • User inputs are implicit too
  • No connection between the states and inputs!

A road to state machine #5

type user =
  | Anonymous
  | TemporaryUser(string)
  | LoggedInUser(userData)
type userInput =
  | SetTemporaryName(string)
  | LogIn(userData)
  | LogOut;

Explicit state and inputs

A road to state machine #5

nextState = transition(currentState, input)

combinations = states * inputs

Transition function

A road to state machine #6

Visualising

Anonymous

Temporary user

Logged in user

SetTempName

Log in

Log in

Log out

A road to state machine #6

Visualising

A road to state machine #7

let transition = (user, input) => {
  switch (user) {
  | Anonymous =>
    switch (input) {
    | SetTemporaryName(name) => TemporaryUser(name)
    | LogIn(data) => LoggedInUser(data)
    | LogOut => user
    }
  | TemporaryUser(_) =>
    switch (input) {
    | LogIn(data) => LoggedInUser(data)
    | SetTemporaryName(_) | LogOut => user
    }
  | LoggedInUser(_) => ...
  };
};

Implementing transition

A road to state machine #7

let transition = (user, input) => {
  switch (user, input) {
  | (Anonymous, SetTemporaryName(tempName)) => TemporaryUser(tempName)
  | (Anonymous, LogIn(userData)) => LoggedInUser(userData)
  | (TemporaryUser(_), LogIn(userData)) => LoggedInUser(userData)
  | (LoggedInUser(_), LogOut) => Anonymous
  | _ => user
  };
};

Avoid _ pattern match

A road to state machine #8

Use as reducer

[@react.component]
let make = () => {
  let (state, dispatch) = 
    React.useReducer(transition, Anonymous);
  
  <button onClick={_ => dispatch(LogOut)}>
    {React.string("Log out")}
  </button>;
};

A road to state machine #9

Use in your UI

<div>
  {(switch (state) {
    | Anonymous => "You can't post questions."
    | TemporaryUser(userName) =>
      "Post with your temporary name: " ++ userName
    | LoggedInUser({userName}) => 
      "Post with your user name:" ++ userName
  }) |> React.string}
</div>;

State machine Recap #1

  • States - variant (discriminated union)
  • Only one state at a time
  • Start in initial state
  • Input (event) - variant (DU)
  • Transition - new state from current state + input
  • It is just a design pattern (no lib required)
  • Can be introduced just in a small part of your app
  • Can be used in any language/environment
  • Better support in functional languages 
    • Variants/pattern matching
    • Compiler warnings - exhaustive match

State machine Recap #2

Benefits

  • Protect against invalid states
  • Protect against invalid state transitions
  • Account for all edge cases (trivial...)
  • Better understand the domain
  • Discover potential problems in spec of business rules
  • IT IS EASY - follow the pattern, focus on the behaviour
  • Fun to visualize 

Use cases

  • Boolean flags
    • isValid, isLoading, isError, 
  • Options
    • error, validResponse etc. 
  • A flood of If-checks
type apiRequest('t) = {
  isLoading: bool,
  data: option('t),
  error: option(string),
};

Thank you! ❤️

Questions?

A road to state machines

By margaretkru

A road to state machines

  • 77
Loading comments...

More from margaretkru