David Khourshid

Infinitely Better UIs with Finite Automata

Conference talk at React Rally 2017. 25:33 long, 56.8K views on YouTube. Year inferred from upload date. Topics: state machines, statecharts.

Watch on YouTube (ReactRally)

What is this talk about?

“If you go on Wikipedia, you're going to see this definition of the mathematical model of a deterministic finite state machine.”

“Finite state machines fall into the automata theory category, which is related to combinatorial logic and also has relations to category theory.”

From the opening of the talk.

Key takeaways

  1. Keep that in mind as we go through what deterministic finite automata really is. 8:00
  2. We always have an initial state, which in this case is just a normal ghost. 11:00
  3. The idea is that by having a declarative representation of your finite state machine. 18:00
  4. The first one is that you could represent your finite state machines as hierarchical states. 20:00

Quoted from the transcript.

Full transcript

From YouTube auto-captions. Punctuation and capitals were restored automatically. Technical terms were normalized; some errors remain. 4,212 words.

0:00 Introduction

Thank you, so show of hands. How many of you think that developing user interfaces is actually easy? Alright, so no one's hands got up, maybe one or two. But the truth is, we all know that developing user interfaces is not easy. In fact, it's pretty difficult. And, you know, working at Microsoft, I completely understand this. Developing user interfaces is a lot more complex than we give it credit for. Especially as a. You know, if you talk to back in engineers, they might think of the front end as just something that. Oh, we'll just, you know, throw some CSS on there, which, you know, makes problems worse. But developing user interfaces comes with a lot of problems on their own. And these could usually be traced down to the behavior of app state and app actions, even for simple apps where you have two states downloading or paused.

And, of course, you know, it doesn't exactly work. And sometimes we just assume that the user is going to follow our direct needs, exactly, so it's sort of a two sided problem. First, we trust the user, and the user trusts us, right? We think that the user is going to take the easiest way and the most happy pathway through our path or through our app. But that's usually never the case. And so today I want to talk to you about an idea that's an extremely important idea because it's been around for a long time. It's not an original idea and it's used in so many different applications, such as software, embedded hardware, electronics, things that keep you alive even. And it's called deterministic finite automata, or finite state machines. So I really pull a while ago. And I asked how many people are actually familiar with finite state machines, or use them in design and developments? Most of you said no. Some of you are familiar with it, and a very small percentage of you actually use it in code, which is pretty awesome.

Because many years ago, we actually are not week. As I was dead, well, I was not born yet. But we. Software developers use state machines in order to develop applications, and this concept can be extended to user interfaces as well. So I want to explain in very easy terms what a finite state machine is, and. So if you go on Wikipedia, you're going to see this definition of the mathematical model of a deterministic finite state machine. My favorite part is this it explains that a finite state machine is a quintuple. And alright, if you're trying to teach someone something, never start with, it's a quintuple. You know, we're gonna be completely lost, have no idea what you're talking about. So, finite state machines fall into the automata theory category, which is related to combinatorial logic and also has relations to category theory. This could make the concept seems really, really foreign to a lot of people, and also extremely difficult.

Now. The beauty of React, since this is a ReaCtS conference, is it makes us rethink. We rethink about best practices, we rethink about just the way we think about developing software. Where everything's componentized, everything's hierarchical, data flows in a single direction, and it makes it easier to reason about data. So I want to do a little thought experiment with you. Let's say that we wanted to create a really, really simple app, let's say that we wanted to create a button. That's just, you know, fetch of goats, it fetches an image of a goats right, it calls Flickr or something, and eventually you get an image of a goat, right. That's all your app does. You're probably thinking right now like, yeah, that's actually pretty simple to implement, you know? I could think immediately how I want to do this. First, we're using component state because we're not gonna get fancy and use Redux quite yet.

We're also, you know, gonna have an events handler that fetches the Goat. So once we get the Goat, we put it in state and then our render function just gonna show the Goat. Pretty simple, right? Here's a problem what if a number of things happen? What if the Goat search fails, right? Well, if we get an error from the server? What if the user decides that he or she really wants a Goat and clicks the button repeatedly, like, just give me the goat. And so what's gonna end up happening is you're gonna have the photo of the goat, and then another goat loaded in another goat. Because, you know, it's a race condition. So what you can do is disable the button, but then that's a problem, too. Because the user could just say, Oh, I'll just undo Siebel the button. You know, it's really easy to get around apps that way. Also, what if we want a custom loading message or a custom error message?

We have a little bit of work to do now. So now in our state, let's, and this is a typical way, we think, right, let's add some boolean flags, right? Let's say, well, we'll keep track if there's an error, and we'll keep track if we're in the middle of searching for our goats. Also in our events handler. Let's make sure that if we're searching for a goat, let's not try to get another goats right. And let's also indicate, if the search fails, that there's a Goat error and display an error message accordingly. So, on that button, we'll say, Hey, the Goat failed to load, so do you want to retry? Our code? Now? Got a lot bigger, so before it was like, this, don't try to read it by the way, I'm just showing the size. And now we have this. Yeah, well, it's odd you can't really see this check because it's on a black background.

But this is called the bottom-up approach to software development, and this is what most of us have either done or are still doing today. And there's a reason for it. It's because it's simple and it works, but there's problems with this in a bottom-up approach. Our events handlers, so, for example, when we click a button and we try to, you know, fetch the goat. We have so many possible things that could happen in that event because of the business logic involved, so now our business logic is decentralized, present in so many different events. Handlers, units all over our code. This makes it really difficult to understand exactly what's happening to our code, and this means our codes can contain bugs. And adding features is going to be an absolute nightmare. Have you ever tried to add a feature to a legacy app or an existing app that you worked on? Where you know if it's a week old, it is a legacy app?

Play from 0:00

7:00 Water break now with React…

Doing that makes it just extremely difficult. Because you have to remember which parts of the code that you know that you have to touch and that whatever state you're. Modifying is touching, and this also makes it really difficult to test as well. Water break now with React, React has a very simple and a very performant data flow model, where since components are nested from their parents. We have props and we have set state. This sort of, and, you know, I mean, though, criticism to React, but this sort of exacerbates our problem with how we develop applications. And the reason for this is because as soon as we need to go to, you know, a different state, that our sorry at different components. That's not part of the tree. Now we have just a flurry of different options that we have to choose from. And so this thing, this makes things a lot more complicated.

And I know some of you are going to go to me and say, well, well, actually, David, you could, you could, probably, you know. Just use props and set state for this to maybe use a callback prop, like, I know I could do this. I could also drive from Florida to Utah, but I decided to take a flight because it was a lot simpler. It's more direct, you know, we like going from point A to point B in the shortest path possible. So the main takeaway from that is this. It's that user interfaces are graphs. They're, you know, they're not strictly trees, they're graphs, which means that things can connect to other things in a non hierarchical fashion. And I know some of you might also come to me and say, well, actually, my application is a tree and flows perfectly down. I'm still right because trees are grafts - so don't at me. So keep that in mind as we go through what deterministic finite automata really is.

Play from 7:00

9:00 In Pac-man, we have the ghost…

And we're going to be using probably the simplest example, which is a traffic light, to explain what this is. So let's break down all of these three confusing words. First of all, finite refers to the number of possible states we have in our application. In a traffic light, we have three green, yellow, red, right then we have at Amida, which is just a fancy word for a predetermined sequence. Something that's built in, something that mechanically will always happen in a single way, for example, green will always go to yellow, yellow will. Always go to red, and red will always go to green, because, you know, if green goes directly to red, people die. So we want to make sure that doesn't happen. And deterministic finite automata are also deterministic. And what this means is that, given the current state in action, you will know what the next state is going to be all the time. So when the timer goes off, it will go from green to yellow, and then yellow to red, and then red to green.

And we could predict this to happen every single time in Utah. These timers are like 20 minutes each smoothing. So we could also consider this a finite state machine. These two things are the same thing, so if you've heard finite state machines, this is exactly what we're talking about. Just to make things a little less confusing, all right, so now let's talk about designing state machines. And fair warning there's a little bit of graph theory in here. And yeah. So let's take Pac-man, for example. In Pac-man, we have the ghost, which could only exist in one of three states. First as a normal ghost that's chasing Pac-man, or Mrs.. Pac-man, then as a ghost, that, you know, once Pac-man eats a cherry, it runs away. And then, if the ghost is eaten, then, you know, the ghost becomes dead, or what ghosts are already dead. I don't know, it becomes eyeballs, okay, so in a graph, remembering, you know, you're like, weather graph is.

We have three nodes or states. These are the finite number of states, but that we represent in the finite state machine. We always have an initial state, which in this case is just a normal ghost. We have the edges between the notes, which are the transitions between the states. Now these have arrows, which makes this a directed graph. Why? Because we're going from one direction to another, and then we have actions that cause these transitions. Just like I talked about once Pac-man gets a pill, did I say, cherry, I'm sorry, I meant pill? Then it becomes, you know, a scared ghost you eat revived. Or the ghost could go back to a normal goat, or, yeah, the scare goes could go back to a normal ghost. Once the timer goes off. So let's go back to our original.

Play from 9:00

12:00 The first one is that you could represent your finite state…

Example before we do anything else, we could use two powerful tools that require no code. It's just pencil and paper. I say pencil instead pen, because you will make mistakes, I make mistakes, and with pencil and paper, you could draw this out as a state transition diagram. So, considering our original example, first, we're in an idle state, right? We're waiting for something to happen from the user, and the only thing the user could do is click. Once that happens, we go into a loading state and then this fetch could either resolve or reject. And so when that happens, we go to one of the two states, either Goat or arrow state. And then once we do a click action again, we go back to loading. Alright, when you? Well, when I was reading about these state transition diagrams, they're commonly referred to as STDs, but honestly, just call it state transition diagrams.

Now, you might find this sort of notation vaguely familiar, and that's because designers inherently work with state machines all the time. It's sort of a natural way of thinking. They might call it like user flowcharts or something like that, where a user can go from one state to another of the app, and they would design each state of the app. So, this is a powerful application of state machines because they provide a common language for both designers and developers alike. And I don't need to tell you how powerful that is. So now that we've designed our state machine, let's talk about how we develop them. If you were to look, you know, as computer science books or whatever, and read up on, how do you declare that Li represents a graph? Then you're going to see an adjacency list, which is just a mapping of states to states that they're connected to. Unfortunately, this doesn't give us all the information that we need because, you know, we need to know what actions have occurred.

So what I propose is this it's a. A machine can be represented as a mapping of states, such as I alone, Encode or error, and then each one of those states could be represented as a mapping of actions. To the next states. And so if you thought that these finite state machines were difficult to implement, here's a transition function. It's just. A lookup function where we look up the current state and then we look up the action that has been performed upon that state. And so, if you wanted this magical finite state machine library, here you go. 24 bytes, you know, I don't even need to put it on the npm or anything, you could just memorize it. If you gzip it, it becomes 44 bytes, so don't gzip it all right? So now let's talk about how we could put this back in our application, let's refactor it a lil so. Now in our set, in our state, our component state, we're gonna represent the state as two things.

We have the finite state, and then we have whatever data we get. And then we're going to have this thing called commands. And these commands you could think of as side effects. And I borrowed this from Elm because Elms architecture has a very powerful way of abstracting these side effects in a declarative, pure way. And that's when you update. Instead of just saying here's the next components I'm going to render, you say here's the next component, and here's the command that I also want to run once you render that component. So our transition method in our state is going to do the exact same thing that transition function that we showed did. It's also going to grab the next command. And then, you know, if you were at the last talk, you could see that. You could pass a callback to set state and this is a very powerful way to execute side effects right from the comfort of set state. And so I know you could use Redux for this, you could use MoB X, RxJS, whatever, but there's there's this natural beauty of just using set state, especially in React.

And I'm sure Ryan Florence would approve, which is why I named this pattern Ryan Florence in the state machine [Applause]. Now, in our event handler, it becomes extremely simple. Why? Because when we get a goat, we just send off a resolve action, and when we, when we get an error, we just send off a reject action. The beautiful part of this, compared to our last thing, is that no matter how many other states we use, no matter how complex our application gets, this is never going. To change, it's never going to grow, and that's the important part. And because of that, we could actually grab that state instead of random boolean flags, and we could declaratively render our view. And I'm just using a, you know, a map, or a object, whatever you want to call it. I know that sometimes you use switch, but honestly, I like representing things, as you know, just objects. So now let's talk about testing and visualizing our state machines, because this is another powerful application of it.

And shoot, oh wow, it's resolving host right there, we go, come on, you know what? I prepared it here. So because we could represent our state machine as a JSON object instead of switch statements. Or IF statements or boolean flags or anything like that. I could do some really cool things like representing it as a visual graph. So, you know, I could change this, and well, I guess it doesn't change. But anyway, I promise you it works. I just have a few bugs to fix. This is something called Cytoscape, and I tried to use D3, but D3 doesn't really handle the nested grass and things like that. But the idea is that by having a declarative representation of your finite state machine. There's a lot of applications that you could do there, it is that you could do from it, and also testing is more powerful too because of this. Why? Because, you know, when you're using Storybook are using just snapshots.

You want to do two things first, you want to get all of your states. All that is object out keys right on your, you know, on your object. And then, you know, finding all of the transitions, that's just higher rating through each of the states. And then you could go crazy and use Dijkstra or Breadth-First search, or any of those other crazy algorithms that you've only used for interview questions. And you could actually use that to find the shortest path between the initial state and every other state, and that way, you could automate unit testing and integration testing now it would. It would not be right for me to talk about all the benefits of finite state machines without talking about some of their trade-offs. To one of the most obvious trade-offs is that let's. Go back to our traffic light example, let's say that we have a know simple traffic light that goes from green to yellow to red. And now, let's say, our boss comes in and says, Hey, you know how we live in Florida, we have hurricanes, we have power outages because of thunderstorms.

You know, sometimes Florida man crawls on the power poles and takes it down. So we need this flashing red state whenever the power goes out. All I did was add one state, and you see what happened. Now we have three more transitions, so imagine if we had even more states? We have to add even more transitions. And this is what's known as state explosion. I. I was creating the slide at the airport. That was not a good idea anyway. There's this thing called statecharts that solves this, and what statecharts are our hierarchical finite state machines? And they were invented by a guy named David Harel, so they're also known as Harel statecharts. And again, this was in 1987, so it's a very, very old concept that's definitely useful today. There's three main benefits of statecharts. The first one is that you could represent your finite state machines as hierarchical states.

So now, instead of saying, you know, we have all these dates interconnected, let's imagine this as two states. Both the normal operation of the traffic light and the flashing red thing, and so now we could just say when normal has a power outage, go to the flashing red. And so there's two rules with hierarchical states, the first one is when you enter a state, you also enter all the initial sub states. So when you enter normal, you're actually in normal green, and then things progresses normal, normal green, normal yellow, normal red. And then when the inner machine cannot handle in the action, such as a power outage, it sends it up to the parents and asks for help. Sort of like, like talking a god or something. And then it would take that normal, and normal would say, Hey, I know how to handle this.

And then it was just transition to flashing red. So this simplifies things a lot. Because we could put as many states as we want inside normal, and the number of transitions out going to flashing red will never. Change. There's also the concept of concurrent states, which is that a state machine could be in more than one state at the same time. The most obvious example of this is a text editor, where you could have bold underline italics, right? Each of these could be on and off, and each of these could, you know, have basically their own state machine, right? And I know what you're thinking like, you know, I thought we were being deterministic, I thought we could only be in one state at a time. What is this? Is? It's like, you know, Schrodinger, a state machine where we don't know if our apps alive or dead. But the reason this works is because each of the state machines are orthogonal, which is just a fancy word that means they don't touch.

And one of my favorite features is history states. To imagine you have a payment flow where the users entering a payment method to buy their a card or a check. And then the user goes to a review page and then goes back, we want to remember if the user chose card or check. And that's what his three states are good for. So I actually wrote a library for this called XState, because all the other steep names were taken. So I just put an X in front, but it actually makes sense because X stands for a transition, or between states or something. And so this tiny library handles transitioning from both hierarchical states and concurrent states in ways that you would expect. So, just like that same machine before, if we are in bold off and we toggle bold, we're gonna be in four states at the same time, bold on, italics off, underline off and list bullets.

And the best part is it's represented also as a JSON object. We just have to extend our original syntax a little bit so that we could have both states. Which are the nested states and on which is the mapping of actions to other states? Now, there's a lot of advantages to using statecharts, such as having precise semantics, which allows you to make them declarative. Having a rich, expressive notation for describing these statecharts. And many other things where you could describe complex user interfaces in a completely declarative way. Now, the my main. Inspiration for this, and where I got the most information, is from this book called Constructing the user interface with statecharts.

Play from 12:00

24:00 Thankfully, there's other resources for this…

It was written in 1997 by Ian Horrocks. And I really want to recommend it to you. But if you go on Amazon, it's like six hundred seventy five dollars, so maybe hold off or put it as a business expense, I don't know. Thankfully, there's other resources for this, including the original paper and two excellent articles by Guillaume Arash and Adam Salope. Which don't necessarily talk about statecharts exactly, but they definitely points in that direction. And as I was walking, I saw the sign, so I thought it was extremely appropriate that, even though it's covered in bird. It's a very important message and that's that we wants to stop maintaining state in an unpredictable way. And so I, I really encourage you. Whether you're using it just for design, just for testing, or if you're going the extra mile and you're using state machines for actual development, that you find state machines.

And thinking of your data as a transition between two different states, connected with an action that, you know, you start to represent your data in a way that's robust, testable, maintainable, and easier to reason about. So, with that said, thank you, React rally. You could find me everywhere at @DavidKPiano.

Play from 24:00

Related talks