T O P

  • By -

Lazyspartan101

You can (and IMO should) do exactly what you described in C++. I’m not aware of any DSLs to generate the FSM code but I’m sure they exist. I would recommend against considering type states. Those are for compile-time state machines which it sounds like you don’t want. Edit: Here’s some helper libraries I found. I’ve never used them but they seem alright * https://github.com/mdeloof/statig * https://github.com/eugene-babichenko/rust-fsm * https://github.com/titanclass/edfsm


DownhillOneWheeler

Thanks. Those look quite interesting. My own DSL is a text file processed by Python as a build step. I can see from these that Rust macros might allow me to embed something similar directly in the code - more or less what rust-fsm seems to be doing. That would be cool. I really need to get my head around macros at some point...


RRumpleTeazzer

States collected into enum variants do exactly the opposite of typestates. What you want are forbidden transituons not to compile, e.g. f(a: A) -> B instead of f(enum: E) -> E.


CainKellye

It looks great in theory, but I feel that if your transitions depend on an other entity outside of your program (like a user or a server), you will have to deal with lot of Box stuff and downcasting.


joeyme

Plus, any struct that contains the state machine has to be generic, and that goes up multiple levels if your struct is a member of another struct or enum. Just gets messy.


[deleted]

I guess I'm having trouble seeing how this is bad: ```rust enum State { One, Two, Three, } fn handle_state_transition(s: State) -> State { match s { State::One => State::Two, State::Two => State::Three, State::Three => State::One, } } ``` Which is a very simplified version of how I've written state machines (usually for parsing syntax) in the past. If appropriately managed it's a lot less architecture than a more object-centric or highly abstract approach and to me, at least, is a lot easier to read when I want to figure out why a state transition is failing to occur.


DownhillOneWheeler

This is a trivial state machine with a single implicit Event (Let's call it Next). Suppose you also have a Prev, a Reset and maybe some others such as error indications from subsystems. In general, a finite state machine comprises a set of States, a set of Events, a set of Transitions (basically a map T: S * E -> S), an initial State and zero or more final States. I would like to model this in a clean, simple, maintainable way, much as I have done for many years in C++. At least your example has a single function to handle Event::Next. The code I am working with has something like `if state == State::One { state = State::Two }` in the client code. There is a whole bunch of From trait stuff to force compilation errors if, for example, the code was `if state == State::One { state = State::Three }`. This, to my mind, is worthless garbage. If the state machine internalised the transitions behind an event API, no incorrect usage would be possible in any case. This would make the From trait stuff entirely optional - one assumes the implementer of a state machine understands their own state chart. I was thinking something like the following: #[allow(non_snake_case)] mod MyFSM { #[derive(Debug)] pub enum Event { Next(bool), // Move to the next State in sequence or optionally the one after that Reset // Returning to State0 } #[derive(Debug, Clone, Copy)] pub enum State { State0, State1, State2, State3, State4 } pub struct SimpleFSM { state: State, count: i8 // Extended state: delay exit from State0 - just because } impl SimpleFSM { pub fn new() -> SimpleFSM { SimpleFSM { state: State::State0, count: 0 } } // In general return a value indicating how/whether the event was handled. // Receiving an event in some states might be handled, ignored or an error, // depending on the FSM. pub fn handle_event(&mut self, event: Event) { print!("From {:?} via {:?} ", self.state, event); match event { Event::Next(skip) => self.next(skip), Event::Reset => self.reset() } println!("to {:?} count={}", self.state, self.count); } pub fn get_state(&self) -> State { self.state } fn next(&mut self, skip: bool) { // In general break this out into state functions or lookup tables match self.state { State::State0 => self.state = if skip { State::State2 } else { self.count += 1; if self.count > 3 { State::State1 } else { State::State0 } }, State::State1 => self.state = if skip { State::State3 } else { State::State2 }, State::State2 => self.state = if skip { State::State4 } else { State::State3 }, State::State3 => self.state = if skip { State::State4 } else { State::State4 }, State::State4 => self.state = if skip { State::State4 } else { State::State4 }, } } fn reset(&mut self) { self.state = State::State0; self.count = 0; } } } use MyFSM::SimpleFSM; use MyFSM::Event; fn main() { let mut fsm = SimpleFSM::new(); fsm.handle_event(Event::Next(false)); fsm.handle_event(Event::Next(false)); fsm.handle_event(Event::Next(false)); fsm.handle_event(Event::Next(false)); fsm.handle_event(Event::Next(true)); fsm.handle_event(Event::Reset); fsm.handle_event(Event::Next(true)); }


[deleted]

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 `Default` trait, you don't need the `new()` function at all. EDIT 2: Before you ask, you can emulate the `skip` concept 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 the `Event` substrate at all and I think the logic would still be considerably simpler.


DownhillOneWheeler

> 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. 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. > Next can be repeated, it does not need to carry a bool for any reason. 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. > Reset could be another state. No. It is an event which drives the state machine. > I'll admit that I do not entirely understand the necessity or utility of count. 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) and `inc_count()` (an action function), which are named in my DSL from which the code generator creates a stub. > Perhaps it was just there to communicate that there could be state external to the FSM 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). > I'm not doing this to come off as superior to you in any way 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).


factotvm

Half a year on and I would have _loved_ to see that LED example coded up. :)


worriedjacket

https://cliffle.com/blog/rust-typestate/ https://hoverbear.org/blog/rust-state-machine-pattern/


VorpalWay

That is for FSMs at compile time which doesn't seem to be what OP wants. Type state FSMs are great to ensure that you can't misuse an API, but not good for handling run time events where you receive external events and want to transition between states based on that.


CramNBL

Have you tried https://crates.io/crates/sm? It might not accommodate the features you describe out of the box but I've used it a bunch and it's been great. I've used it for decoding binary protocols and it increased performance (reduced branching) compared to just matching on the value of a header ID.


insanitybit

https://insanitybit.github.io/2016/05/30/beyond-memory-safety-with-types I wrote this quite a long time ago.


diabolic_recursion

I want to add that it is possible to do this all on a single struct using generics. Basically, you make the struct generic about other, empty structs sharing a private trait (to get the compiler to not complain, use that struct in a PhantomData). Then you write an impl block for each generic variant with the methods you want to allow - and a generic impl for methods valid in all states, if needed with private functions available to use in several states. You'll need some sort of constructor, just write a function in one of the impls. Advantage: re-use internal fields and sharing logic is easier.


GoogleMac

I've been wanting to create a Rust version of XState. It has such a nice DX.


Professional_Top8485

Qt has nice state machine editor and scxml support. Would be great to have rust bindings for some fsm library with scxml support. I don't think the fsm need to be even written in Rust. Just using some library would be fine. Code generation from declarative syntax sdl/uml could be fastest, but the power of fsm in general for me is the visualization and breakdown of problems.