8 ms·
Simpler UI Logic With Finite State Machines
- ketzo 7y agoAt 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).
- deleted 7y ago[deleted]
- DecoPerson 7y agoI 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.
- layoutIfNeeded 7y agoSwift enums and std::variant would work the same.
- bcherny 7y agoThe 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.
- danenania 7y ago"{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.
- aratakareigen 7y agoI 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.
- crdrost 7y agoRight, 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.
- danenania 7y agoRight, 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.
- bjz_ 7y agoTrouble 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.)
- IshKebab 7y agoThat is a ridiculously niche term to worry about confusing with. I would put money on 99% of programmers having never heard of it.
- dmitriid 7y ago> 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... :)
- e12e 7y ago> 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.
- shhsshs 7y agoThis translates well to backend code too. Really any programming.
- pmoriarty 7y agoThis makes me wonder if something similar can be done with petri nets. https://en.wikipedia.org/wiki/Petri_net https://en.wikipedia.org/wiki/Petri_net
- gimboland 7y agoPhilippe 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/ https://www.irit.fr/recherches/ICS/ [2] https://www.irit.fr/recherches/ICS/softwares/petshop/ico.html https://www.irit.fr/recherches/ICS/softwares/petshop/ico.htm...
- cwingrav 7y agoPetri nets are often used for theorem proving about state machines and systems. So, yeah, good instincts!
- jsf01 7y agoI 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?view=react&typescript=false https://overmindjs.org/guides/intermediate/06_statecharts?vi...) and xstate which I believe was their inspiration (https://xstate.js.org/ https://xstate.js.org/).
- DoctorOetker 7y agoI 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...
- girishso 7y agoThat 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).
- masklinn 7y agoA 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.
- andreiursache 7y agoAnother 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-app-using-stateflow.html https://www.mathworks.com/help/stateflow/ug/analog-trigger-a...
- rumanator 7y agoThis 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.
- blargmaster33 7y agoThis is why we old timers look upon you copy pasta Devs with distain
- deleted 7y ago[deleted]
- Wyndtroy2012 7y agoHow does this differ from a flux pattern?
- fnordsensei 7y ago"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 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.
- jwr 7y agoI 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.
- AstralStorm 7y agoAs 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.
- jwr 7y agoI'm not sure what you mean? Much of my core.async code deals with things like network requests.
- sansnomme 7y agoCore.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.
- slifin 7y agoI 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
- beaker52 7y agoThis 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:
- NicoJuicy 7y agoSomething 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.
- bbmario 7y agoMore info: http://alumni.media.mit.edu/~jorkin/goap.html http://alumni.media.mit.edu/~jorkin/goap.html
- dkersten 7y agoFor 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.
- omneity 7y agoFSMs (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.
- AstralStorm 7y agoThe 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.
- ahartmetz 7y agoIME 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.
- benzoate 7y agoSwift 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).
- kolodny 7y agoNice, 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/6fa6aa34a711d36e9de01cec44091557 https://gist.github.com/kolodny/6fa6aa34a711d36e9de01cec4409...
- pcmaffey 7y ago> 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.
- thecleaner 7y agoIsnt this state based transition what react/redux tries to accomplish ?