6 ms·
> I'm sure there are patterns that could help with this. Indeed there are. Again any Turing complete language can do anything another language can with a suffi
by dwohnitmok 6y ago
> I'm sure there are patterns that could help with this.
Indeed there are. Again any Turing complete language can do anything another language can with a sufficient dose of design patterns (in the limit design patterns just recreate another language). So everything I say about "classes" and "state machines" needs to be taken as talking about level of effort not possible vs. impossible, since the former, being Turing complete in most cases, can always model the latter.
But to return to the meat of your questions:
> Why? Is that because you need to know if a transition is valid for a given state?... Not sure what you're refering to by this.
In a manner of speaking yes this is about validity of transitions, but it only shows up when you're making a bigger state machine from smaller ones.
What I meant by "state machine composition" and "important part of representing state machines is that you can inspect their state" relates to the combinatorial explosion of states that occurs when you build a larger state machine from smaller state machines.
When you combine state machines naively you get a new state machine that is the combination of all its child states (the n-tuple of all the child states). These usually (but not always) are not all valid states at the parent state level. That is usually, at the parent state level, only some subset of the combination of every possible child state is a valid overall state.
This means, if you're thinking of things purely as state machines all the way down, you don't want your child state machines to be black boxes. Instead, you want to be able to form relationships between the different child states to be able to constrain them in different ways. In a statically typed language with algebraic data types this might show up as various different sum types that are subsets of tuples. In a language without sum types this often ends up being basically the visitor pattern. In a dynamically typed language this might show up as various assertions about the overall n-tuple. Either way, you need some way to "open up" the child states to inspection by the parent state.
Now sometimes, your parent state truly is the n-tuple of all its child states, i.e. the child state can be any state at all and this is still a valid parent state. At the UI level, this is usually true for anything a user would be unlikely to care about if the page were to refresh and that state was reset, for example the state that governs the animation of a flashing button, or a drop-down menu being open or closed. This is what I meant when I said "black boxes that are stateless from the perspective of the overall system." From the parent system's perspective, if the child component is a black box, you could substitute that child component with a completely stateless placeholder (e.g. a static button) and the overall system would not be in an incorrect state.
And it is precisely these places where it is most convenient to use opaque child states to model things.
However, most "significant" things in a UI are things where we do care about constraining the n-tuple of child states to a subset. We often want certain parts of the UI to gate other parts of a UI (e.g. greying out certain parts of a UI), or want certain actions in one part of the UI to cascade in a specific way across the rest of the UI.
Don't get me wrong, getting back to the beginning of my reply, I'm by no means saying that this is impossible with opaque classes. After all there have been people making perfectly functional UIs with opaque classes for a long long time. It's merely more annoying and bug-prone.
If all your child states are opaque, then all this subsetting of valid states is implicit in control flow rather than explicit in data definitions. And as Fred Brooks put it: "Show me your flowchart [control flow] and conceal your tables [data definitions], and I shall continue to be mystified. Show me your tables, and I won't usually need your flowchart; it'll be obvious."
I'm not sure what you would want from a citation? Somebody other than me saying that the important part of representing state machines is being able to inspect their state? Would you accept prior art? Languages that are explicitly built to model state machines, such as TLA+, generally expose the state explicitly by default and build all their reasoning facilities around manipulating the state directly (TLA+ is indeed generally not an executable language, but there aren't many executable languages I can think of at the moment that really go all-in on making everything a state machine like TLA+ does and so aren't great illustrations of what pure state machines are like; Elm does this as well, but Elm and Redux are the same core approach so it doesn't really count in a discussion about Redux).
- nsonha 6y agolet's back off a bit. I agree with the other commenter's point, which is the really important thing: > Redux's bad ideas do not come from the fact that it models a state machine, it comes from the fact that it models it poorly So to me it doesn't really matter whether class is the best tool to implement state machine. If the argument is "you can do it with class/function/language feature x, however it's a lot of work to achieve composition or whatever", then the same can be said about redux: it kind of looks like a state machine, but in order to have composability/validation/static types etc, you still have to apply a bunch of "patterns". So it's just as effective (or ineffective) as the approach of rolling your own state machine.
- dwohnitmok 6y agoBut that's precisely where I strongly disagree. Redux's problems do not come from a poor implementation of a state machine, but rather the interactions between state machines and stateful, non-state machine representations. Again, Redux is the purest expression of a state machine possible in a general-purpose programming language (as opposed to a specialized one like TLA+). It maps one-to-one with the usual textbook definition (technically textbooks don't have a rigorous definition for state machines, but rather finite state machines, but usually by "state machine" we just mean you take a normal FSM and just relax the finite bit by introducing some infinite component into the overall state such as an arbitrarily long list). The store is the state, the actions are transitions, and the reducer is the state transition table. Redux's infelicities come precisely from the fact that it needs to interact with things that are not modeled as state machines! Namely, interacting with black box children and the outside world (mostly sending stuff to the outside world, Redux comes with stuff out of the box to deal with just one-way inputs from the world). And Redux doesn't come with ways out of the box to deal with those so you need to layer more stuff to make it all play nicely together. But you need at some point to interact with them to work with the JS ecosystem so you need that stuff. If you had state machines all the way down you don't need any particular patterns other than the fundamental state machine description! Especially with Typescript's structural types, everything just works (you just pull out chunks of your store into little stores and pull chunks of your big reducer out into little reducers, it's literally just cut-and-pasting chunks of your code and giving it a new name and making new state machines all the way down). The friction comes from interacting with React, dealing with the outside world, and figuring out various decisions of what to do about data structures that the JS stdlib doesn't decide for you out of the box (e.g. what immutable library should one use). Even the choice of mutable vs immutable is something that is decoupled from Redux proper (it turns out for incidental reasons it's easier for type systems to properly scope local effects with immutable data structures than mutable ones, but that's more an artifact of Typescript than Redux, see e.g. Rust's type system for a counterexample). If all of that was just more state machines (and the stdlib issues were taken care of) then Redux would be absolutely lovely. And that's not just a pie-in-the-sky hypothetical. E.g. Elm is basically what Redux would be if everything was state machines and works great in all the places Redux doesn't (Elm's infelicities in turn are of a different variety that stem from some limitations of its type system to properly express some of the structural types you'd like state machines to have, in particular a lack of structural union types that often requires duplicating the state transition table in certain ways as you break a larger state machine down into smaller state machines).