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
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

Elements 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
