4 ms·
I don't understand why you would code these explicit state machines when you can just write normal code that is much more readable. The state machine example th
by gsliepen 2y ago
I don't understand why you would code these explicit state machines when you can just write normal code that is much more readable. The state machine example they start with could be written as:
while (true) {
wait();
fill();
finish();
}
I don't think the approach from the article would have any benefits like less bugs or higher performance.
- skavi 2y agoallows trivially mocking effects for testing
- sdenton4 2y agoI tend to think of state machines as becoming important when you're forced to deal with the unpredictably of the real world, rather than just pummeling bits until they repent. You've got some complicated Thing to control which you don't have full visibility into... Like, say, a Bluetooth implementation, or a robot. You have a task to complete, which goes through many steps, and requires some careful reset operations before you can try again when things inevitably don't go according to plan. What steps are needed for the reset depends on where you're at in the process. Maybe you only need to retry from there steps ago instead of going all the way back to the beginning... The states help you keep track of where things are at, and more rigorously define the paths available.
- MrJohz 2y agoFor a very simple example like this, your version will probably be okay, but it has its own set of problems: * It's difficult to introspect the current state of the system. If I were to build an API that fetches the current state, I'd need to do something like add an extra state that the different functions could then update, which makes all the code more messy. By turning this into an explicit state machine, I can encode the states directly in the system and introspect them. * Similarly it's often useful to be able to listen to state transitions in order to update other systems. I could include that as part of the functions themselves, but again, if I just encode this operation as an explicit state machine, the transition points fall out very nicely from that. * Here there is no branching, but state machines can often have complicated branching logic as to which state should be called next. It's possible to write this directly as code, but in my experience, it often gets complicated more quickly than you'd think. This is just a simple example, but in practice a bottle filler probably has extra states to track whether the machine has been turned off, in which case if it's in the `fill` state it will switch to the `finish` or `wait` state to ensure the internal machinery gets reset before losing power. Adding this extra logic to a state machine is usually easier than adding it to imperative code. * In your version, the different functions need to set up the state ready to be used by the next function (or need to rely on the expected internal state at the end of the previous function). This gives the functions an implicit order that is not really enforced anywhere. You can imagine in a more complicated state machine, easily switching up two functions and calling them in the wrong order. In OP's version, because the state is encoded in such a type-safe way, any state transitions must handle the data from the previous state(s) directly, and provide all the correct data for the next state. Even if you were to get some of the states the wrong way round, you'd still need to correctly handle that transition, which prevents anything from breaking even if the behaviour is incorrect.
- joshka 2y agoThere's definitely a missing part of this which talks about when to use this sort of approach. The answer is often when there's non trivial amounts of stuff that happens between the end of one method and the start of the next which is in control of the external system. That said, I often argue that async/await solves the majority of that problem by implicit modeling of the state machine while keeping the code readable.
- crq-yml 2y agoIt's a formal model that we can opt into surfacing, or subsume into convenient pre-packaged idioms. For engineering purposes you want to be aware of both. It's way easier to make sense of why it's relevant to write towards a formalism when you are working in assembly code and what is near at hand is load and store, push and pop, compare and jump. Likewise, if the code you are writing is actually concurrent in nature(such as the state machines written for video games, where execution is being handed off cooperatively across the game's various entities to produce state changes over time) most prepackaged idioms are insufficient or address the wrong issue. Utilizing a while loop and function calls for this assumes you can hand off something to the compiler and it produces comparisons, jumps, and stack manipulations, and that that's what you want - but in a concurrent environment, your concerns shift towards how to "synchronize, pause and resume" computations and effects, which is a much different way of thinking about control flow that makes the formal model relevant again.
- IshKebab 2y agoOne very common reason is to make the code non-blocking. In fact Rust's async/await system works by converting the code into a state machine. Unfortunately Rust doesn't have proper support for coroutines or generators yet so often you'll want to use a hand written state machine anyway. Even if it did, sometimes the problem domain means that a state machine naturally fits the semantics better than using a control flow based approach anyway.
- asimpletune 2y agoBecause to reason about things becomes harder as the stakes are raised. We had to implement paxos for distributed systems in college and my partner I started over probably about three times trying to code it normally. Then we switched to just focusing on defining states and the conditions that transition between them and our solution became much easier to code.
- deleted 2y ago[deleted]
- baq 2y agoThat’s true for everything. If you feel confident you can safely refactor and modify a state machine encoded in this way, go for it. Most of us who have seen the trenches don’t feel confident and gladly accept tool assistance.
- eddd-ddde 2y agoYeah, and while you are at it drop the types and just use normal assembly, that way you won't even have UB!
- marginalia_nu 2y agoOne benefit is that if you persist the state on transition, you can create a program that survives restarts and crashes. The pattern is very useful if you have tasks with very long run times that need to be orchestrated somehow. There are also some parsers that can be very rough to implement in an imperative fashion.
- imtringued 2y agoThat's only because the diagram is completely wrong and your code is wrong accordingly, because it implements the incorrect diagram. Usually a wait state has a self referential transition. You don't perform the wait once, you keep waiting until the condition for transitioning to the fill state is true. The next problem is that you are doing polling. If you were to implement this on a microcontroller then your code cannot be called from an interrupt and let the microcontroller go to sleep to conserve power.
- theptip 2y agoIf you try to implement an actual state machine (or even interlinked ones like TCP) in this style you will have a very bad time. The FSM model is restrictive but this makes it much easier to exhaustively cover and validate all state combinations. To be concrete, the kind of code you end up writing in your non-FSM approach (for a non-trivial example where you have, say, an RPC per state transition or other logic between the transitions) is def finish(): if state == WAIT: #error elif state == FILL: … And worse, you need to validate your state in each transition, which ends up being duplicative and error-prone vs. just specifying your transitions once and then depending on them to enforce that the states are valid. FSMs are a great pattern when they apply cleanly.
- creatonez 1y agoThat's just a minimalistic example. You might want to say, add a progress bar, or persist the state in an SQL database, which you can't do by letting the CPU ram through a huge code block.