5 ms·
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 caugh
by DecoPerson 7y ago
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.
- 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.