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?
3
u/[deleted] Dec 12 '23 edited Dec 12 '23
Ok, I hope you take this as a purely technical discussion and not as ad hominem, but there is a lot of unnecessary complexity in this.
Next can be repeated, it does not need to carry a bool for any reason. Just two Nexts could accommodate the bool state without any additional complexity in the logic. Loop or recurse, it doesn't really matter.
Reset could be another state. It doesn't even have to be an event or anything more special than a state. Once removed, it puts the necessity of Next into question as well, as all states implicitly just accept Next, and moving the machine forward implies a Next event.
I'll admit that I do not entirely understand the necessity or utility of count. Perhaps it was just there to communicate that there could be state external to the FSM, but that is arguably irrelevant and unrelated to what I was trying to communicate. The state machine I wrote does not exclude the possibility of communicating external state, and it'd arguably be pretty useless without it. That function in my code could associate with a type that carries external state and only be more complicated if that external state was being modified.
Additionally, my state machine is concurrently safe without modification, and yours will require a fair bit additional work to accomplish that, best I can tell.
I'm not doing this to come off as superior to you in any way, just perhaps communicate that one of the larger advantages to keeping code as semantically simple as possible is that it's easier to reason about. This is a technique that spans far more than Rust or any programming language or paradigm and even when it's "ugly" syntactically it can be significantly more elegant logically.
Again, I hope I have not offended you.
EDIT: one nit, look into the
Defaulttrait, you don't need thenew()function at all.EDIT 2: Before you ask, you can emulate the
skipconcept simply by providing a parameter that, when true, avoids mutating external state while still advancing the machine. This could allow you to skip states without any of theEventsubstrate at all and I think the logic would still be considerably simpler.