Consider the pattern where there are several states registered with a dispatcher and each state knows what state to transition to when it receives an appropriate event. This is a simple state transition pattern.
struct Dispatcher {
states: HashMap<Uid, Rc<RefCell<State>>>,
}
impl Dispatcher {
pub fn insert_state(&mut self, state_id: Uid, state: Rc<RefCell<State>>) -> Option<Rc<RefCell<State>>> {
self.states.insert(state_id, state)
}
fn dispatch(&mut self, state_id: Uid, event: Event) {
if let Some(mut state) = states.get_mut(&state_id).cloned() {
state.handle_event(self, event);
}
}
}
trait State {
fn handle_event(&mut self, &mut Dispatcher, Event);
}
struct S0 {
state_id: Uid,
move_only_field: Option<MOF>,
// This is pattern that concerns me.
}
impl State for S0 {
fn handle_event(&mut self, dispatcher: &mut Dispatcher, event: Event) {
if event == Event::SomeEvent {
// Do some work
if let Some(mof) = self.mof.take() {
let next_state = Rc::new(RefCell::new(S0 {
state_id: self.state_id,
move_only_field: mof,
}));
let _ = dispatcher.insert(self.state_id, next_state);
} else {
// log an error: BUGGY Logic somewhere
let _ = dispatcher.remove_state(&self.state_id);
}
} else {
// Do some other work, maybe transition to State S2 etc.
}
}
}
struct S1 {
state_id: Uid,
move_only_field: MOF,
}
impl State for S1 {
fn handle_event(&mut self, dispatcher: &mut Dispatcher, event: Event) {
// Do some work, maybe transition to State S2/S3/S4 etc.
}
}
With reference to the inline comment above saying:
// This is pattern that concerns me.
S0::move_only_field
needs to be an Option
in this pattern because self
is borrowed in handle_event
, but I am not sure that this is best way to approach it.
Here are the ways I can think of with demerits of each one:
- Put it into an
Option
as I have done: this feels hacky and every time I need to check the invariant that theOption
is alwaysSome
otherwisepanic!
or make it a NOP withif let Some() =
and ignore the else clause, but this causes code-bloat. Doing anunwrap
or bloating the code withif let Some()
feels a bit off. - Get it into a shared ownership
Rc<RefCell<>>
: Need to heap allocate all such variables or construct another struct calledInner
or something that has all these non-clonable types and put that into anRc<RefCell<>>
. - Pass stuff back to
Dispatcher
indicating it to basically remove us from the map and then move things out of us to the nextState
which will also be indicated via our return value: Too much coupling, breaks OOP, does not scale asDispatcher
needs to know about all theState
s and needs frequent updating. I don't think this is a good paradigm, but could be wrong. - Implement
Default
for MOF above: Now we canmem::replace
it with the default while moving out the old value. The burden of panicking OR returning an error OR doing a NOP is now hidden in implementation ofMOF
. The problem here is we don't always have the access to MOF type and for those that we do, it again takes the point of bloat from user code to the code of MOF. - Let the function
handle_event
takeself
by move asfn handle_event(mut self, ...) -> Option<Self>
: Now instead ofRc<RefCell<>>
you will need to haveBox<State>
and move it out each time in the dispatcher and if the return isSome
you put it back. This almost feels like a sledgehammer and makes many other idioms impossible, for instance if I wanted to share self further in some registered closure/callback I would normally put aWeak<RefCell<>>
previously but now sharing self in callbacks etc is impossible.
Are there any other options? Is there any that is considered the "most idiomatic" way of doing this in Rust?
Dispatcher
even do? The only way I can make sense of this is that you have multiple state machines with differentUid
s and the dispatcher picks one to handle each event. Why do you keep all the "used" states in theHashMap
-- will a state ever be re-entered? How? – trentclunsafe
code here so what safety has been broken ? Also how do you suggest tackling this problem ? – ustulationS0
will call the destructor ofMOF
. If you've movedMOF
out ofS0
but not yet replaced it, that destructor would have access to undefined memory. That's why the compiler disallows it. I'm not saying you have unsafety, just explaining why that field always has to be valid otherwise there would be unsafety. There's no universal answer. See the closed RFC for lots of discussion – Shepmaster