In PUrsuit of

finite states

Farzad Yousef Zadeh

Senior software engineer 🇫🇮

Formerly:

Aerospace engineer 🚀  astrophysicist 🌌

Implicit vs Explicit state

Finite state vs Infinite state

  • Decoupled from implementation

  • ported to several platforms

logic

  • Modeled

We will talk about

  • Visualized

The What

Scene #1

renderer (state) = view

Component library / Framework

State management tool
Tool specific approaches

Local state

Global state

store.dispatch({
  type: "UPDATE_TITLE",
  payload: "Poland"
})
@action onClick() {
  this.props.title = "Poland"
}
const [title, setTitle] = useState("")

setTitle("Poland")
this.state = {
  title: "";
}

this.setState({title: "Poland"})
new Vuex.Store({
  state: {
    title: ""
  },
  mutations: {
    updateTitle: (state, payload) {
      state.title = payload.title
    }
  }
})

One purpose

multiple approaches

State management tools

are containers

deal with the architecture                         (flux, push based, pull based)

tell you how to store states                         (single source, distributed)

tell you how to update states             (Mutable, immutable, observable)

Current state flow

After adding modeling layer

A flashback to

GUI history

GUI is event-driven

Alan Kay, the Dynabook, 1972

GUI is built upon events and messages

Listen to events and execute actions (side effects)

event + action paradigm

The problem

Scene #2

facetime bug

MS calculator bug

Event handler's logic based on previously happened events

Press the (-) button

Context aware event handlers

Constructing the User Interface with Statecharts

Ian Horrocks 1999

The MAIN problem

Implicitly handling the state

Event handlers

Mutations

Side effects

Asynchrony

button.addEventListener("click", ...)
this.setState({ ... })
new Promise(...)
window.fetch(...)
if (typeof index === "boolean") {
  addBefore = index;
  index = null;
} else if (index < 0 || index >= _.slideCount) {
  return false;
}

_.unload();

if (typeof index === "number") {
  if (index === 0 && _.$slides.length === 0) {
    $(markup).appendTo(_.$slideTrack);
  } else if (addBefore) {
    $(markup).insertBefore(_.$slides.eq(index));
  } else {
    $(markup).insertAfter(_.$slides.eq(index));
  }
} else {
  if (addBefore === true) {
    $(markup).prependTo(_.$slideTrack);
  } else {
    $(markup).appendTo(_.$slideTrack);
  }
}

Problems of implicit state modeling

IMpossible states

{
  value: "",
  valid: false
}
{
  value: "fars",
  valid: false,
  isValidated: true
}
{
  value: "",
  valid: false,
  isValidated: true
}
{
  value: "farskid@gmail.com",
  valid: true,
  isValidated: true
}
{
  value: "",
  valid: false,
  isValidated: false
}
{
  value: "fars",
  valid: false
}
{
  value: "",
  valid: false
}
{
  value: "farskid@gmail.com",
  valid: true
}
value.length  = 0  > 0
valid true false
isValidated true false
value.length = 0, valid: false, isValidated: false
value.length = 0, valid: false, isValidated: true
value.length > 0, valid: false, isValidated: true
value.length > 0, valid: true, isValidated: true
value.length  = 0  > 0
valid true false
isValidated true false
value.length = 0, valid: true, isValidated: false
value.length = 0, valid: true, isValidated: true
value.length > 0, valid: false, isValidated: false
value.length > 0, valid: true, isValidated: false

2

2

2

4

valid states:

4

impossible states:

impossible states

v.length
valid
isValidated
2^3

with bad modeling,

your code complexity grows faster than the domain complexity

with impossible states,

You need to test cases that won't event happen in real life

An impossible state is where you tell users to restart

Problems of implicit state modeling

Mutual exclusivity

{
  submitting: boolean;
  submitError: string | undefined;
}
submitting: false, error: undefined
submitting: true, error: undefined
submitting: false, error: Error
submitting: false, error: undefined

Editing

Submitting

Failed

Succeeded

{
  submitting: boolean;
  submitError: string | undefined;
  isSuccess: boolean;
}
submitting: true, error: undefined, isSuccess: false
submitting: false, error: undefined, isSuccess: false
submitting: false, error: Error, isSuccess: false
submitting: false, error: undefined, isSuccess: true

To avoid

impossible states

and

mutually exclusive behaviors

from happening

we add guards

GUARDS on

handler level

function handleChange(newValue) {
  if (!isValidated) {
    setValidated(true);
  }

  if (newValue.length === 0 || !validateEmail(newValue)) {
    setValid(false);
  } else {
    setValid(true);
  }

  setValue(newValue);
}

Guards increase

cyclomatic complexity

Number of independent paths a program can take 

More guards: 

HIGHER CYCLOMATIC COMPLEXITY

LESS PREDICTABLE LOGIC

if, else, 
while, for, 
switch, case

Harder to track logic


if (typeof index === "boolean") {
  addBefore = index;
  index = null;
} else if (index < 0 || index >= _.slideCount) {
  return false;
}

_.unload();

if (typeof index === "number") {
  if (index === 0 && _.$slides.length === 0) {
    $(markup).appendTo(_.$slideTrack);
  } else if (addBefore) {
    $(markup).insertBefore(_.$slides.eq(index));
  } else {
    $(markup).insertAfter(_.$slides.eq(index));
  }
} else {
  if (addBefore === true) {
    $(markup).prependTo(_.$slideTrack);
  } else {
    $(markup).appendTo(_.$slideTrack);
  }
}
<Button 
  type="submit" 
  disabled={isDisabled}
>
  Submit Form
</Button>

GUARDS on

rendering level

The Solution

Scene #3

Discovering Finite states

Thinking about states explicitly

Think in states explicitly

in the input example

Empty

Finite state

inFinite state

{value: ""}

Invalid

{value: "fars"}

Invalid

{value: ""}
{value: "farskid@gmail.com"}

Valid

Type

InputState = "Empty" | "Invalid" | "Valid"
function transition(state, event) {
  switch(state) {
    case "Empty":
    case "Invalid":
    case "Valid":
      switch(event.type) {
        case "TYPE":
          return validateEmail(event.payload) 
            ? "Valid" : "Invalid"
        default:
          return state;
      }
    default:
      throw Error("Impossible state")
  }
}
onChange(e) {
  setInputContext(e.target.value)
  setInputState(transition(inputState, "TYPE"))
}
<input
  type="text"
  onChange={e => {
    setInputContext(e.target.value);
    setInputState(
      transition(inputState, "TYPE")
    );
  }}
/>;

{
  inputState === "Invalid" && 
  <p>Enter a valid email address</p>;
}

conditional rendering based on finite state

{
  !valid && isValidated && 
  <p>Enter a valid email address</p>;
}

Reasoning based on implicit state

Reasoning based on explicit finite state

Tooltip/modal/dropdown

type State = "Opened" | "Closed"

Button

type State = 
| "Normal"
| "Hovered"
| "Active.Idle"
| "Active.Focused"
| "Disabled"

Range Input / Progress

type State = 
| "Min"
| "Mid"
| "Max"

const context = {
  min: number,
  max: number,
  value: number
}

Elements have finite states

type Promise =
| Pending
| Settled.Fulfilled
| Settled.Rejected

const Context = {
  resolvedValue?: any;
  rejectedError?: any
}

Time based side effects have finite states

">

but this explodes

state explosion

Scaling

Scene #4

statecharts

David HAREL (1987):

A VISUAL FORMALISM FOR COMPLEX SYSTEMS* 

Extends FSM model

States with relations

side effects as first class citizens

event: Click

effect: search

state: idle + event: Click

state: searching

state: idle + event:  Click

state: searching + effect: search

Normal Event + Action

finite state machine

statechart

Statechart implementation library

Xstate

Based on SCXML

JSON definition

Built-in Visualizor

Rewrite the input

in statecharts

{
  initial: "empty",
  context: { value: "" },
  on: {
    TYPE: [
      {
        actions: "updateValue",
        target: "empty",
        cond: "isEmpty"
      },
      {
        actions: "updateValue",
        target: "valid",
        cond: "isValid"
      },
      {
        actions: "updateValue",
        target: "invalid"
      }
    ]
  },
  states: {
    empty: {},
    invalid: {},
    valid: {}
  }
}

statechart

for a basic input

Writing the Software is EASY

Maintaining is HARD

{
  states: {
    empty: {},
    invalid: {},
    valid: {
      initial: "checking",
      states: {
        checking: {
          invoke: {
            src: "checkPromise",
            onDone: "available",
            onError: "notAvailable"
          }
        },
        available: {},
        notAvailable: {}
      }
    }
  }
}

previous input +

a check for available email when email is valid

{
  states: {
    valid: {
      initial: "debouncing",
      states: {
        debouncing: {
          after: {
            DEBOUNCE_DELAY: "checking"
          }
        },
        checking: {
          invoke: {
            src: "checkPromise",
            onDone: "available",
            onError: "notAvailable"
          }
        },
        available: {},
        notAvailable: {}
      }
    }
  }
}

previous input +

debouncing the checking service

{
  states: {
    valid: {
      entry: "initFetchController",
      exit: "abortFetchRequest",
      initial: "debouncing",
      states: {
        debouncing: {
          after: {
            DEBOUNCE_DELAY: "checking"
          }
        },
        ...
      }
    }
  }
}

previous input +

network cancellation

where is Implementation details then?

import { Machine } from "xstate";

// Abstract declarative JSON
const statechartJSON = {...}

// a statechart object from the JSON
const statechart = Machine(statechartJSON);

// Add implementations
statechart.withConfig({
  actions: {}, // updateValue
  services: {}, // checkPromise
  guards: {}, // isEmpty, isValid
  delays: {} // DEBOUNCE_DELAY
});
type Entity = 
  (ctx: Context, e: Event) => any;

statecharts for interavtivity

a Dragging box

Interactions with statecharts

Released
GRAB
Grabbed
MOVE
Dragging
MOVE
RELEASE
{
  initial: "released",
  context: {
    mousePositionInBox: { x: 0, y: 0 },
    boxPositionInContainer: { x: 0, y: 0 }
  },
  states: {
    released: {
      on: {
        GRAB: {
          target: "grabbed"
        }
      }
    },
    grabbed: {
      entry: [
        "saveMousePositionInBox",
        "saveBoxPositionInContainer",
        "prepareBoxStyles",
        "moveBox"
      ],
      on: {
        MOVE: "dragging"
      }
    },
    dragging: {
      entry: [
        "saveBoxPositionInContainer",
        "moveBox"
      ],
      on: {
        MOVE: "dragging",
        RELEASE: "released"
      }
    }
  }
}
Released
GRAB
Grabbed
MOVE
Dragging
MOVE
RELEASE
onMouseDown = (event) => {
  sendEvent({
    type: "GRAB",
    data: getMousePositionInBox(event)
  });
}

onMouseMove = (event) => {
  sendEvent({
    type: "MOVE",
    data: getBoxPositionInContainer(event)
  });
}

onMouseUp = () => {
  sendEvent("RELEASE");
};

Proxy mouse events to events consumed by the statechart

box.onmousedown = function(event) {
  // (1) prepare to moving: make absolute and on top by z-index
  box.style.position = 'absolute';
  box.style.zIndex = 1000;
  // move it out of any current parents directly into body
  // to make it positioned relative to the body
  document.body.append(box);
  // ...and put that absolutely positioned ball under the pointer

  moveAt(event.pageX, event.pageY);

  // centers the ball at (pageX, pageY) coordinates
  function moveAt(pageX, pageY) {
    box.style.left = pageX - box.shiftX + 'px';
    box.style.top = pageY - box.shiftY + 'px';
  }

  function onMouseMove(event) {
    moveAt(event.pageX, event.pageY);
  }

  // (2) move the ball on mousemove
  document.addEventListener('mousemove', onMouseMove);

  // (3) drop the ball, remove unneeded handlers
  box.onmouseup = function() {
    document.removeEventListener('mousemove', onMouseMove);
    box.onmouseup = null;
  };
};
{
  initial: "released",
  context: {
    mousePositionInBox: { x: 0, y: 0 },
    boxPositionInContainer: { x: 0, y: 0 }
  },
  states: {
    released: {
      on: {
        GRAB: {
          target: "grabbed"
        }
      }
    },
    grabbed: {
      entry: [
        "saveMousePositionInBox",
        "saveBoxPositionInContainer",
        "prepareBoxStyles",
        "moveBox"
      ],
      on: {
        MOVE: "dragging"
      }
    },
    dragging: {
      entry: [
        "saveBoxPositionInContainer",
        "moveBox"
      ],
      on: {
        MOVE: "dragging",
        RELEASE: "released"
      }
    }
  }
}

Before

AFTER

Additional benefits

Scene #5

{
  initial: "released",
  context: {
    mousePositionInBox: { x: 0, y: 0 },
    boxPositionInContainer: { x: 0, y: 0 }
  },
  states: {
    released: {
      on: {
        GRAB: {
          target: "grabbed"
        }
      }
    },
    grabbed: {
      entry: [
        "saveMousePositionInBox",
        "saveBoxPositionInContainer",
        "prepareBoxStyles",
        "moveBox"
      ],
      on: {
        MOVE: "dragging"
      }
    },
    dragging: {
      entry: [
        "saveBoxPositionInContainer",
        "moveBox"
      ],
      on: {
        MOVE: "dragging",
        RELEASE: "released"
      }
    }
  }
}

Box is released at first

When it's released, it can be grabbed

As soon as it's grabbed, we remember mouse position and box position, prepare its styles and move it.

When it's grabbed, it can move

As soon as it's moving, we update its current position and move it.

When it's moving, it can be released

As long as it's moving, we keep moving it which means continuously updating its position.

Statecharts read like English

Statecharts visualize logic

Generate directed Graph

Showcase state paths

Paths can be used by QA team to test for edge cases

cross competence teams

onboarding

Statecharts visualization in pull requests

Statecharts decouple logic from implementation

Abstract declarative JSON

{
  initial: "A",
  states: {
    A: {},
    B: {}
  }
}

Implementation

statechart.withConfig({
  actions: {},
  services: {},
  delay: {}
})

PLatform api

Next time someone asked how hard can it be?

Answer: let's draw its statecharts and see!

RECAP

Scene #6

levels solution complexity and problem complexity

Recap

finite state vs infinite state

statecharts for practical modeling complex applications

Implicit vs explicit state management

A new modeling layer

make impossible states, impossible

Recap

STATECHARTS ARE A FRAMEWORK/tools AGNOSTIC APPROACH

STATECHARTS CAN BE USED FOR COMPONENT LEVEL STATE

STATECHARTS CAN BE USED FOR APP LEVEL STATE

statecharts for knowledge sharing and communication

Avoid mutual exclusivity problems

THINK in STATES

Thank you!

Dziękuję Ci

 

Slides at:

SkyTalks Poland 2019

Check these out

The World of statecharts

xstate
@xstate/react
@xstate/fsm