erikras.com
HomeAbout

Finite State Machines in ReScript

ReScript's strong typing makes it especially well suited for defining Finite State Machines.

Posted in Coding, ReScript, Finite State Machines
March 22, 2021 - 8 min read
Finite State Machines in ReScript

After hearing my friend, David 🎹, go on and on about [finite] state machines and the value they can provide to us as frontend programmers, a few months ago, I was able to actually use his library, XState, in anger at work, and the result was nothing short of glorious!

It eliminates a whole class of bugs!

👆 This is a phrase that has come out of my mouth very often in the last couple of years since being baptized into the Holy Church of TypeScript. I have spread the Good Word far and wide, and even created a meme which I have used multiple times on PRs with my team.

1308736441753767937

ReScript

ReScript, the artist formerly known as BuckleScript and Reason, takes "It eliminates a whole class of bugs" to the next level. Quoth the docs:

ReScript has no pitfalls, aka the type system is "sound" (the types will always be correct). e.g. If a type isn't marked as nullable, its value will never lie and let through some undefined value silently. ReScript code has no null/undefined errors.

Okay, you have my attention! I read through the docs one day like it was a Jack Reacher novel, very excited to see the sort of tradeoffs ReScript was making.

In my n00b experience so far, the interop edges where ReScript must consume – moreso than provide – third party JavaScript libraries feels a bit awkward...at least more awkward than modern 2021 TypeScript which has huge community support (so the comparison is not really fair).

Since acquiring this itch, I've been looking for a simple example project to try out my ReScript skills.

Twitter's "Follow" Button

Last week, I noticed an interesting thing about the "Follow" button on Twitter's webapp. Rather than having four states, not-following, following, not-following-hover, and following-hover, it actually has five! There's a special state when you've just clicked "Follow", but are still hovering over the button.

1372838015656136708

Perfect! This is a job perfect for a state machine, and also tiny enough that maybe I could implement it in ReScript!

At the time of this writing, there are virtually zero search results that come back for "XState ReScript". There is one article about ReScript and state machines called Modelling domain with state machines in ReasonML, from almost a year ago by Margarita Krutikova, in which she mentions XState, but codes in ReasonML ReScript. That is what I shall attempt to do, as well. One could imagine a ReScript interop layer for XState existing (ReXState? 🦖), but it doesn't seem to at the moment.

"Follow" Button State Machine

I admit to usually rolling my eyes at the point of David 🎹's talks where he recommends:

Start with a sheet of paper or a whiteboard, and draw out the states you need.

"I can see the Matrix and visualize all the states just looking at the code", I think smugly to myself. However, this time, darned if he wasn't right! I struggled with considering all the interactions writing just code (plus, this coding language is foreign to me), so I found great benefit in drawing up my state machine by hand first. Of course I used Excalidraw, not paper – I'm not a caveman!

Follow Button Finite State MachineThe clicks without hovering are there for completeness (and maybe accessibility?).

Notice that from not-following-hover, it's pretty hard to get to following-hover (with red background and "Unfollow" text), when you would think would be the next state; you have to:

  1. Click
  2. Mouse out
  3. Mouse over again

ReScript Implementation

Types

Let's start with our types:

type state =
| Following
| FollowingHover
| FollowingNeverMouseOut
| NotFollowing
| NotFollowingHover
type event = Click | MouseOver | MouseOut
type style = Highlight | Danger | Hover | None

The actual Twitter "Follow" button doesn't have a style for NotFollowingHover, but I decided to improve on that and make a Hover style.

getText()

What should the text of the button be in each state? Notice that you can "fall through" just like you can with a switch-case statement in JavaScript if you leave out the break.

@genType
let getText = (state: state) => {
switch state {
| NotFollowing
| NotFollowingHover => "Follow"
| FollowingHover => "Unfollow"
| Following
| FollowingNeverMouseOut => "Following"
}
}

Notice the @genType. One fun fact that made me want to attempt adopting ReScript is that: IT GENERATES TYPESCRIPT TYPES FOR YOU!!! 🤯

It's not the most beautiful TS ever, but it gives you the typings and then defers the calls to the generated JavaScript. For example, here's an excerpt of the TypeScript the code above generated:

// tslint:disable-next-line:no-var-requires
const followButtonBS = require("./followButton.bs");
// tslint:disable-next-line:interface-over-type-literal
export type state =
| "Following"
| "FollowingHover"
| "NotFollowing"
| "NotFollowingHover"
| "FollowingNeverMouseOut";
// tslint:disable-next-line:interface-over-type-literal
export type event = "Click" | "MouseOver" | "MouseOut";
// tslint:disable-next-line:interface-over-type-literal
export type style = "Highlight" | "Danger" | "Hover" | "None";
export const getText: (state: state) => string = function (Arg1: any) {
const result = followButtonBS.getText($$toRE161065015[Arg1]);
return result;
};

However, more interesting, is the JavaScript that it generates. Check this out:

function getText(state) {
switch (state) {
case /* FollowingHover */ 1:
return "Unfollow";
case /* NotFollowing */ 2:
case /* NotFollowingHover */ 3:
return "Follow";
case /* Following */ 0:
case /* FollowingNeverMouseOut */ 4:
return "Following";
}
}

All nicely documented so you can follow along.

getStyle()

Now we declaratively define how the state relates to the style (think, "CSS class") of the button.

@genType
let getStyle = (state: state) => {
switch state {
| FollowingHover => Danger
| Following => Highlight
| NotFollowing => None
| NotFollowingHover
| FollowingNeverMouseOut => Hover
}
}

Pretty similar to getText(). Notice that the "fifth state", FollowingHover, is special in both functions.

The Machine

Here's where it gets fun! The machine is a simple mapping from state to state via events. This is just the code that represents the arrows in the drawing...nothing more.

@genType
let machine = (. state:state, event: event) =>
switch state {
| NotFollowing => switch (event) {
| Click => Following
| MouseOver => NotFollowingHover
| MouseOut => state
}
| NotFollowingHover => switch event {
| Click => FollowingNeverMouseOut
| MouseOut => NotFollowing
| MouseOver => state
}
| FollowingNeverMouseOut => switch event {
| Click => NotFollowingHover
| MouseOut => Following
| MouseOver => state
}
| Following => switch event {
| MouseOver => FollowingHover
| Click => NotFollowing
| MouseOut => state
}
| FollowingHover => switch event {
| MouseOut => Following
| Click => NotFollowingHover
| MouseOver => state
}
}

...which compiles nicely – in milliseconds! – to this efficient JS code:

function machine(state, $$event) {
switch (state) {
case /* Following */ 0:
switch ($$event) {
case /* Click */ 0:
return /* NotFollowing */ 2;
case /* MouseOver */ 1:
return /* FollowingHover */ 1;
case /* MouseOut */ 2:
return state;
}
case /* FollowingHover */ 1:
switch ($$event) {
case /* Click */ 0:
return /* NotFollowingHover */ 3;
case /* MouseOver */ 1:
return state;
case /* MouseOut */ 2:
return /* Following */ 0;
}
case /* NotFollowing */ 2:
switch ($$event) {
case /* Click */ 0:
return /* Following */ 0;
case /* MouseOver */ 1:
return /* NotFollowingHover */ 3;
case /* MouseOut */ 2:
return state;
}
case /* NotFollowingHover */ 3:
switch ($$event) {
case /* Click */ 0:
return /* FollowingNeverMouseOut */ 4;
case /* MouseOver */ 1:
return state;
case /* MouseOut */ 2:
return /* NotFollowing */ 2;
}
case /* FollowingNeverMouseOut */ 4:
switch ($$event) {
case /* Click */ 0:
return /* NotFollowingHover */ 3;
case /* MouseOver */ 1:
return state;
case /* MouseOut */ 2:
return /* Following */ 0;
}
}
}

Demo Time

The following button (pun intended) is written using the logic described above, with the actual JavaScript compiled from ReScript.

Notice how there are two states where it reads "Following", the one right after you've clicked it, and the button is still gray, and the one after you've moused away and the button is purple.

So concludes my first of what will hopefully be many posts about ReScript. Feel free to follow me on Twitter as I ask questions from people smarter than myself and get help learning this promising language.

Discuss on Twitter

© 2023 Erik Rasmussen