Hello!
I am writing a peer-to-peer application and I want to implement the different states the peers can be in. I have seen the various state machine patterns, but my understanding is that they can only be used when the transitions from one state to the other are known statically.
In my case of course, message from a peer come at runtime, so I will probably need something else. I came up with a solution on my own but it is quite convoluted, and I think sub-optimal. So before I spend more time trying to optimise/refactor it I wanted to ask if there are known patterns which fulfill the following (or a "good enough" subject):
- If I write code that makes a wrong state transition, this will be caught at compile time.
- If there is a function like
send_disconnect_msg
I will only be able to call it in states where they are allowed to send a disconnection - Be as efficient as possible. I guess this is not a so well defined requirement. I mean that my implementation should be as close to an implementation by a very good programmer that would never make the above two errors I am trying to catch.
If you are interested, for now I have something like:
enum Event {
Event1,
Event2,
}
struct CommonData;
trait CanDisconnect {};
#[enum_dispatch::enum_dispatch]
trait State {
fn handle_event(self, event: Event) -> CurrentState;
};
struct StateA;
struct StateB;
impl CanDisconnect for StateB {}
struct StateMachine<T> {
state: T,
common_data: CommonData,
}
impl StateMachine<StateA> {
pub fn new() -> Self {
// ...
}
}
impl StateMachine<StateB> {
pub fn new() -> Self {
// ...
}
}
impl<T: CanDisconnect> StateMachine<T> {
fn send_disconnect(&mut self) {
}
}
impl From<StateMachine<StateA>> for StateMachine<StateB> {
// ...
}
impl State for StateMachine<StateA> {
// ...
}
impl State for StateMachine<StateB> {
// ...
}
#[enum_dispatch::enum_dispatch(State)]
enum CurrentState {
StateA(StateMachine<StateA>),
StateB(StateMachine<StateB>),
}
The idea is that if you want to go from one state to the other you will have to do something like StateMachine::<StateB>::from(self)
. So a wrong transition will be caught compile time.
Also, if you want to to make a call to send_disconnect
, you will have to be in a state that is marked with the CanDisconnect
state, otherwise that method will not be available.
The problem is that i since I use enum_dispatch
i need to make a lot of .into()
calls inside the event_handler. And since this is something that might be called quite often, I imagine it might impose a big overhead. Also the code seems to be a bit convoluted.
Another problem, but probably orthogonal, is that since I consume the current state I can't just have a CurrentState
be a member of some other struct, since if i had a &mut
reference to that struct, I couldn't partially move the CurrentState
out. Also I think that the common_data
field is probably moved every time I make a handle_event
call, but I am not sure how move semantics work exactly.