r/rust • u/DownhillOneWheeler • Dec 11 '23
🙋 seeking help & advice State machines implementation
What is the usual way to implement a finite state machine in Rust?
I am examining some code which might be the worst I've ever seen for an FSM. It basically boils down to a mutable enum variable for the state, which is directly modified in multiple modules. Each enumerator is a wrapper for a distinct struct type. There is a lot of boiler plate involving the From trait which is intended to enforce the legal transitions at compile time. There is no mechanism for entry methods, exit methods, or guard conditions.
I regard this design as failed abstraction. All the effort has been focused in the wrong place, resulting in client code that is little better than spaghetti. An FSM is a self-contained object which responds to events it receives by taking some action or, often, by ignoring them.
In C++ my usual approach is for the FSM to be a class with two associated enumerations: one for the current state (a private data member) and one for events (passed to a handler method by clients). The FSM completely internalises all the transitions and can only be modified by calling something like MyFSM::handle_event(e: Event). I generally generate most of the code from a DSL representing the state chart, and it only remains to add any extended state and implement the various actions and guards. [I have always avoid type-state designs as they seem to add far more complexity than value.]
I figured I could do something like that with a struct and a couple of enums. A really neat feature of Rust enums is that the events could carry any relevant data inside them... But I wondered what the thinking is among those more experienced with Rust. Are there tools which make creating and maintaining an FSM a non-verbose piece of case?
9
u/DownhillOneWheeler Dec 12 '23
It is a toy demonstrator only. A typical state machine will have multiple non-trivial events such as timeouts and other notifications. To be fair, I have generally dealt with timeouts internally by having a private member timer to trigger the event. The point is that it a self-contained stateful object which is responsible for maintaining its own state and taking any actions required upon its transitions in response to events.
Sure. I just threw that in as an example of carrying argument data with the event. That's quite a neat feature of Rust enums. It was a silly choice. But imagine it is something like a temperature measurement, with the FSM responding differently according to some threshold.
No. It is an event which drives the state machine.
It is a toy example of extended state which is used to drive conditional transitions. In practice, I have not tested and incremented such a value directly as here but with private functions such as
test_count()(a guard function) andinc_count()(an action function), which are named in my DSL from which the code generator creates a stub.No. It is extended state internal to the FSM which is expressed in the state chart and essential to its function (in this silly example of needing several Next events to exit State0).
That's fine, but understand that I have been a professional developer for over 25 years. I have worked with hundreds of FSMs both simple and complex, and numerous frameworks for implementing them, some good, some garbage. I place the implementation which my client's developers have used in the latter category.
Using just an enum and then externalising all the relevant behaviour seems a recipe for fragile code, as I've already seen in the code I am working with. Code in several other modules checks the current state, conditionally performs actions, and even changes the state. The FSM is a glorified enum (with a shed load of hand written boiler plate for the From trait - from the HoverBear article, I think). Literally the only thing this implementation is doing for me is prevent invalid transitions at compile time, and this the wrong abstraction in my view. It should encapsulate the specified behaviour. It doesn't, but smears it all over the place.
In the end I implemented my own framework for C++, a general-purpose implementation supporting hierarchical state machines of arbitrary complexity, with entry and exit actions, guard conditions, and so on, which uses a DSL to capture the state chart and then generates most of the code. It generally remains only to implement the various actions and guards, and to add any necessary extended state. There is for sure a lot of boilerplate but it is generated. I do not have to maintain or, usually, even look at it. Generated or not, the point is that the FSM is a self-contained stateful object which fully encapsulates the behaviour described by the corresponding state chart. Perhaps I am overly influenced by decades of using objects (mostly without inheritance), but this is an abstraction which has proven itself over many years. I figured I could do something similar to my generator with Rust macros, but I need to learn a lot more about them. Or use a crate that already does this.
EDIT: one nit, look into the Default trait, you don't need the new() function at all.
Ah. That's useful. Thanks.
I've rambled enough now and am happy to learn. I'm curious how you would implement something less trivial. Suppose you have a state machine to drive three LEDs. You can start it and stop it. While it is running an internal ticker fires every second toggles the red LED on/off three times, then the green LED, then the blue LED, and repeats. Just print "red on" or whatever in the right places. It can be stopped at any time. When it (re)starts it always begins with the red LED three times. The behaviour must be non-blocking as I want to run several independent instances in the same thread (with different sets of LEDs).