Finite State Machines in ReScript
ReScript's strong typing makes it especially well suited for defining Finite State Machines.
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.
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.
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!
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:
- Click
- Mouse out
- Mouse over again
ReScript Implementation
Types
Let's start with our types:
type state =| Following| FollowingHover| FollowingNeverMouseOut| NotFollowing| NotFollowingHovertype event = Click | MouseOver | MouseOuttype 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
.
@genTypelet 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-requiresconst followButtonBS = require("./followButton.bs");// tslint:disable-next-line:interface-over-type-literalexport type state =| "Following"| "FollowingHover"| "NotFollowing"| "NotFollowingHover"| "FollowingNeverMouseOut";// tslint:disable-next-line:interface-over-type-literalexport type event = "Click" | "MouseOver" | "MouseOut";// tslint:disable-next-line:interface-over-type-literalexport 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.
@genTypelet 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.
@genTypelet 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.