Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Simpler UI Logic With Finite State Machines (terodox.tech)
185 points by terodox on Dec 23, 2019 | hide | past | favorite | 53 comments


I like the idea of using FSMs for UI logic a lot but this article barely even scratched the surface on the topic. Their “state machine” is basically just a switch statement with some useless boilerplate around it. If anyone’s interested in the topic I’d recommend looking at the statecharts from overmind (https://overmindjs.org/guides/intermediate/06_statecharts?vi...) and xstate which I believe was their inspiration (https://xstate.js.org/).


I agree, I also think FSMs + 3D vector graphics (bezier surfaces etc) with 3rd dimension for time, can replace a lot of javascript code, and any remaining requirement for turing complete languages should be explicitly apps, instead of websites, so the web can start clean up the javascript hogging and tracking mess...


I find "state" to be quite an overloaded term, especially in React land, so I use "stage" instead for the various state machines in our applications. It's caught on in our org and I've heard no complaints.

State machines are great but they get quite complicated when you have two machines interacting at some point. I find this happens quite a bit for complicated UIs, like a data grid with filter and search controls, in-row controls, a preview pane on the side, etc. Things that can be reasonably simple become overly complicated as you try to cram everything into a state machines model.

It's hard to give concrete examples... but it happens often. I even find myself doing it.

Rust enums (sum types) are really good for doing FSMs, as they:

- Allow you to add fields to each state, which makes it easier to understand what each state is for at a glance.

- Prevent you from accessing fields for a different state at the wrong time.

- Make it easy to do nested states, without that nestedness existing for states that don't need it.

- Provide exhaustiveness warnings for if you're not handling a case.

I do wish more languages had sum types.


> State machines are great but they get quite complicated when you have two machines interacting at some point. I find this happens quite a bit for complicated UIs, like a data grid

Even before you get to a data grid, you get an exponential explosion of states. Since most FSMs are not a part of the actual UI frameworks, and are and addon on top of that, you end up with just so much stuff.

I had a rather simple checkout form in the form of "list ticket types, when clicked select number of people, click `reserve`". Sounds like a few states, right? Until you throw in "is user logged in?", "is the user VIP" (requires adding some other tickets with their states), "do we require user consent?", "do we require user info?", "can user use their bonus points?", "is there a min/max amount of tickets allowed?" etc. etc. etc.

Suddenly you have an explosion of states and validations between these states. It's no longer A -> B. It's A -> (if X then B else if then Y C else if Z ерут В) for many if not all state transitions.

And that's just to show screens in the checkout process. Now let's talk about form controls and fields on each page... :)


The number of possible states exploding is indeed a problem with frameworks like React.

One mental model that helps reduce this load, that you alluded to, is avoiding product types where possible in favor of sum types.

Eg. Imaging you have a type describing React props for some component: {a?: number} and {a: number} | {} seem very similar on the surface — they both describe an object with a single field that may or may not be defined. In both cases you have 2 possible states.

But as soon as you add another field, the number of states can grow quickly unless you’re thoughtful: {a?: number, b?: string} has 4 states, while the model you might have been trying to describe — {a: number} | {b: string} — still has just two.

The less states you have, the easier it is to reason about and automatically verify your model. But in practice, people often accidentally introduce product type where they meant to use sums. I see this with Flow and TypeScript-typed React code all the time.


"{a?: number, b?: string} has 4 states, while the model you might have been trying to describe — {a: number} | {b: string} — still has just two."

The problem I've run into with the second approach as the number of states expand is 1 - you end up with tons of boilerplate to discriminate between types, and 2 - it becomes hard to treat objects polymorphically.

I sometimes end up doing {a: number, b?: undefined} | {b: string, a?: undefined} so that I can write `if (obj.a) doSomething()`, but it obviously gets unwieldy with a lot of properties and states. Imo it would be nice if TS did this automatically so that I could check for the existence of any property that might be defined on an object.


Right, TS wants you to go full-on tagged union.

    {type: 'a', a: number} | {type: 'b', b: string}
When you do this you consume it with a switch(x.type) statement, not an if() statement, so that you can write `default: assertImpossible(x.type)` for some function `assertImpossible(y: never): never` that throws an exception of your choice.

This gives clear fail-at-runtime semantics if JS hands you some crap your types weren’t anticipating, but also fail-at-compile-time semantics if your pattern match is no longer exhaustive (i.e. you added a new case, now TypeScript tells you everything you need to change).

In general it's really nice to view any data structure as isomorphic to the control structure that consumes it. “lists” are consumed by for..of loops, “dicts” are consumed by for..of Object.keys() loops, sum types are consumed by switches, objects are consumed by property accesses, etc.


If your code is fully typed, you don't need the default, since you know at compile time that you've handled your cases (Playground link: [0]):

    type Props =
      | { type: 'a', a: number }
      | { type: 'b', b: string }

    function f(props: Props): number {
      switch (props.type) {
        case 'a': return 1
        case 'b': return 2
      }
    }
Unfortunately you're right that this breaks when using `if` instead of `switch` (unless you have a default return at the end of your function, or replace one of your `if`s with an `else`; Playground link: [1]):

    type Props =
      | { type: 'a', a: number }
      | { type: 'b', b: string }

    // Error TS2366: Function lacks ending return statement and return
    // type does not include 'undefined'.
    function f(props: Props): number {
      if (props.type === 'a') {
        return 3
      }
      if (props.type === 'b') {
        return 4
      }
    }
This second case is a bug in TSC, and is tracked in https://github.com/microsoft/TypeScript/issues/18319.

[0] https://www.typescriptlang.org/play/#code/AQ4FwTwBwU2AFATgey...

[1] https://www.typescriptlang.org/play/#code/AQ4FwTwBwU2AFATgey...


Right, tagged unions are what I use, but when you start having many variants, nested tagged unions, etc., it gets quite awkward imo when you need a big ol’ switch statement to get at some property or other instead of just a simple property check.


You can use an `in` check for this, since TypeScript version 2.7 [0].

Here's an example (Playground link: [1]):

    import * as React from 'react'

    type Props =
       | {a: number}
       | {b: string}

    function MyComponent(props: Props): JSX.Element {
      if ('a' in props) {
        return <div>a is {props.a}</div>
      }
      return <div>b is {props.b}</div>
    }

    let x = <MyComponent a={3} />
    let y = <MyComponent b="foo" />

[0] https://www.typescriptlang.org/docs/handbook/release-notes/t...

[1] https://www.typescriptlang.org/play/?jsx=2#code/JYWwDg9gTgLg...


Oh nice! I’ll give that a try, thanks.


Swift handles this exact problem quite elegantly with the "if case let" construct, allowing you to destructure sum types directly within an if statement.

https://alisoftware.github.io/swift/pattern-matching/2016/05...


I don't know TypeScript, but can't you expend a few lines to write a function 'getA : State -> number?'? Paired with the TypeScript equivalent of Swift's / Rust's if-let, it should be just as convenient.


The fundamental problem is that Typescript (and JavaScript) doesn't have tagged unions. In Rust or even C++ using std::variant this isn't a problem.



Every language can do tagged unions with "kind" or "tag" markers and switch statement. But it's going to be verbose without pattern matching/destructuring combo.


Thankfully TypeScript's type system is powerful enough (and JS's syntax flexible enough) to decently simulate pattern matching:

https://gist.github.com/frankpf/cde7f792580f731dfe886ed2d91b... (see the second file for usage)


It’s not that bad, to be honest. Maybe a few more keystrokes than Haskell, but so is everything else in a C-style language.

Definitely a fair point that it is a few more keystrokes, though.


Trouble with "stage" is that is collides with the "stages" in "multistage programming" - so you're kind of adding to overloading in another field! It's also a field that has lots of applicability to the front-end space (eg. server-side rendering, Svelte, etc.)


That is a ridiculously niche term to worry about confusing with. I would put money on 99% of programmers having never heard of it.


Swift enums and std::variant would work the same.


> State machines are great but they get quite complicated when you have two machines interacting at some point.

I suppose this depends a bit on how transitions are triggered.

If an ux even can trigger a change in state machine a, from state a1 to a2, and that transition can fire off an event that trigger a transition in b, from b1 to b2, and that transition fires an event that triggers a transition from a1.... Then you have problems.

It would probably be better to model a and b as a bigger machine c, that models all states and transitions, and allow the various components/widgets to be "views" of this machine c?

Or; make sure that a state transition won't in and of itself fire an event that triggers a transition in another state machine. Ie: a and could both respond to window resize, but should not trigger a window resize.


"If you haven't built a finite state machine, you've built a bad finite state machine."

I've recently started using Fulcro (framework in ClojureScript), where they are first class citizens in the form of "UI state machines."

http://book.fulcrologic.com/#_ui_state_machines

In retrospect it's obvious how much better it is to contain, for example, the logic of logging in and signing up within an FSM.


I am not convinced that explicit state machines are a good way to write code. Using core.async you get an implicit state machine, while preserving the illusion of sequential code, which is much simpler from the developer point of view.


As long as you don't want to interact from said "sequential" code with the world. Then you get to deal with all the fun of asynchronous programming. Including but not limited to race conditions, data corruption and so on.


Core.async supports Go-style channels so you are trading off speed (it has a runtime) for concurrency-correctness. It's a good fit for network requests since the bottleneck is elsewhere.


I'm not sure what you mean? Much of my core.async code deals with things like network requests.


> "If you haven't built a finite state machine, you've built a bad finite state machine."

SMC (State Machine Compiler) is worth bringing up here, which uses a formal language to define a state machine and produces code in one of a dozen+ languages to run it. Makes it impossible to build a bad state machine. I remember using this on some small projects last decade, I'm happily surprised to see it's still maintained.

http://smc.sourceforge.net/ and code at: https://sourceforge.net/p/smc/git-code/ci/master/tree/


> "If you haven't built a finite state machine, you've built a bad finite state machine."

Thank you!

I read the article, and then beyond, and I still don't know what in tarnation distinguishes an FSM from a simple state. Every program has a set of states and transitions. Is the difference simply that in an FSM the programmer has modeled both sets explicitly? And if so, what is the model -- a map from states to available transitions?


The main difference between a FSM and a statechart is the compositionality aspect of the two formalisms. The FSM can be combined using the OR and the AND operators: the AND operator produces a number of states that is equal to the cartesian product of the original states of the two FSMs that are conjuncted. This means that you are limited in the usefulness of the FSM by the number of interactions between systems, because the number of states increases exponentially.

The statechart solves this problem by using a representation that allows both OR and AND avoiding the state explosion. Multiple states can be combined in a super-state, which allows to model common properties of the enclosed states, provided that the internal substates are XOR-ed, i.e. only one of them can be active in a given time. The advantage of the super-states is that they allow the specifier of the system to proceed in a top-down manner by specifying iteratevily the complete behaviour of the system. These super-states can also conjuncted, to model the interactions between the systems and to represent their parallelism. This conjunction creates implicit interactions between the two subsystems, which substitute the need to create the product FSM.

Both FSM and statecharts can represent the same behaviour of a system, but they differ in how easily understandable their representation can be for complex systems.


>I read the article, and then beyond, and I still don't know what in tarnation distinguishes an FSM from a simple state.

Well, FSM is a basic CS concept.

>Every program has a set of states and transitions. Is the difference simply that in an FSM the programmer has modeled both sets explicitly? And if so, what is the model -- a map from states to available transitions?

Yes, and actions on each transition.


I hope more community docs start springing up around Fulcro 3 soon

Fulcro is doing so much innovation in the front end space that goes completely ignored by the wider community because lisp is hard to read and types are the be all end all of programming


> Fulcro is doing so much innovation in the front end space

For someone who is versed in Clojure, what is Fulcro doing in the frontend space that is so innovative?


That looks a whole lot like Elm's Custom/Union/Sum types, except the compiler helps by making `STATE.isValid` redundant (which the programmer might forget to call).


A proper sum type would go way beyond that because each state would only contain (and have access to) the relevant data as well, whereas with a C-style enum the data is "sibling" to the enum, and so you generally get possibly garbage or irrelevant data in every state and the possibility for that garbage to not be properly initialised or overridden on state transition.

And the compiler would further ensures that you are handling every possible state at every level.

One problem I've often encountered with UI state machines is the explosion in error states, and the "proper" handling of invalid state transitons (unexpected operations) not from a technical perspective but from a UI perspective.


FSMs (and HSMs, the hierarchical version) are very useful for UI design, I am afraid the point that the article is trying to make:

> A simpler approach would be to have a single variable for state, and only allows switching between well known states!

Was not reflected in the code, since it's possible to transition directly from "error" to "success" without going through "submitted".

Which makes me wonder: Does anyone know whether there's existing art for proving UIs using something like TLA+? Sounds like in interesting match, if your UI is following a FSM pattern.


At the last company I worked with, our React codebase was very heavy with enums for exactly this reason (thank you Typescript). Anecdotally, it makes reading a new codebase WAY easier: switch(state) is much easier to understand than if (a bunch of flags).


This is a good article to tell everyone that Qt extensively supports finite state machines to simplify UI logic, to the point that it even allows developers to use State Charts XML (SCXML) to CRUD finite state machines that drive UI states.


This makes me wonder if something similar can be done with petri nets.

https://en.wikipedia.org/wiki/Petri_net


Philippe Palanque and the Interactive Critical Systems research group at Toulouse [1] have been ploughing this furrow for years, on the academic side but with industrial applications mainly in avionics. So this kinda stuff is happening, but it's still niche.

See e.g. [2] for more on one of the Petri-net-based formalisms they're using.

[1] https://www.irit.fr/recherches/ICS/

[2] https://www.irit.fr/recherches/ICS/softwares/petshop/ico.htm...


Petri nets are often used for theorem proving about state machines and systems. So, yeah, good instincts!


This is the most basic multi-state machine you could make. You could do this with a typescript enum, giving you compile time type checking "for free".

I've tried this "state machine" approach but the problem with designing a React/UI component to respond to a single "state" value like this is that it's not uncommon to end up wanting _more_ states like "has errors and loading", or "has data and loading". At which point you find you're building lots of similar looking subcomponents which get torn apart every time you add a new attribute to the combination. Sometimes it's easier to deal with the combinatorial number of states by passing in the attributes separately and deal with the combinations of state with if/ternary statements, exactly like in the original example.

Plus you don't have to have the switch statement argument with someone for whom not using them was the only thing they remember from Uncle Bob's Clean Code :grin:


Another good example is how in MATLAB an app UI can be integrated with state diagrams (Stateflow charts)

https://www.mathworks.com/help/stateflow/ug/analog-trigger-a...


Something more advanced, but similar to FSM is GOAP.

It's used in calling AI for bruteforcing possible actions according to a state that a NPC can use.

I recommend to read it/check it out, the AI of FEAR ( which is excellent) is based on it.


For anyone who wants to try it, I played around with GOAP a little last year. Here are a few things I learned that might help someone else get started:

Represent your state space as a graph and run A* over it to find a “path” (or plan) from starting state to goal state. A simple heuristic to use for the A* distance heuristic (which I saw in one of the gamedev talks, I don’t remember if in FEAR but its likely it was) is to simply count how many variables are correct and use that count as the distance heuristic (they used simple boolean variables, if you use numeric values it may be harder, but I suppose you could still just count how many goal variables are met).

I also attempted to use prolog-style logic programming (in clojure’s core.logic) to search for a plan, but I never finished it. I guess I overcomplicated it by being too ambitious. I wanted the actions to be able to have preconditions like “have at least N of X” and the goal to have similar conditions and have the planner keep track of counts. It worked to a degree but I never finished it because it started getting too complex for my limited knowledge of logic programming.



The fun part is when you need to integrate anything that's not synchronous into a classic state machine. It just does not work.

Message passing between FSMs usually does instead and makes them simpler.


IME pure finite state machines are too limited. E.g. on Android, you have a "go back" stack that you can integrate into a custom state machine, but not into most state machine frameworks. Another difficulty is animated transitions. While transitioning, are you in the start state or the end state? It depends on why you're asking!

If you can make it work, a state machine for navigation is great, but it's often not so easy.


Swift is extremely good at this pattern. I’ve been using this pattern a lot with associated values. You remove any ambiguity in the state of your view controller, and any unhandled state generates a compiler error (so long as you doggedly refuse to implement a default handler).


Nice, Swift also handles having all the transition changes as well. I wrote a small post about how to do this: https://gist.github.com/kolodny/6fa6aa34a711d36e9de01cec4409...


> A simpler approach would be to have a single variable for state, and only allows switching between well known states!

In web apps, this is the route, as the browser comes out of the box with a familiar and handy UI for traversing and sharing state.


Isnt this state based transition what react/redux tries to accomplish ?


This translates well to backend code too. Really any programming.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: