vendredi 28 avril 2017

Preferable Pattern for getting around the "moving out of borrowed self" checker

Just to demo my problem, consider this pattern where there are several states registered with a dispatcher, and each state knows what state to transtion next to when it receives an appropriate event. Just 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.
    }
}

As marked in the code via inline comment, S0::move_only_field needs to be an Option in this pattern. However I am not sure this is best way to approach it. Here are the ways I can think of with demerits of each one:

With reference to the inline comment above saying:

// This is pattern that concerns me.

I cannot move it out without putting it into an Option, in handle_event as self is borrowed. So my choices are:

  1. Put it into and Option like here: Feels hacky and everytime i need to check the invariant that the option is always Some otherwise panic! or make it a NOP with if let Some() = pattern and ignore the else clause, but this causes code-bloat. Both, doing and unwrap or bloating the code with if let Some() feels a bit off.
  2. Get it into a shared ownership Rc<RefCell<>>: Need to heap allocate all such variables or construct another struct called Inner or something that has all these non-clonable types and put that into an Rc<RefCell<>>.
  3. Pass stuff back to Dispatcher indicating it to basically remove us from the map and then move things out of us to the next State which will also be indicated via our return value: Too much coupling, breaks OOP, does not scale as Dispatcher needs to know about all the States and needs frequent updating. I don't think this is a good paradigm, but could be wrong.
  4. Implemnt Default for MOF above: Now we can mem::replace it with default while moving out the old value. The burden of panic'ing OR returning an error OR doing a NOP is now hidden in implementation of MOF. 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.
  5. Let the function handle_event take self by move as fn handle_event(mut self, ...) -> Option<Self>: Now instead of Rc<RefCell<>> you will need to have Box<State> and move it out each time in the dispatcher and if the return is Some 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 a Weak<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 ?

Aucun commentaire:

Enregistrer un commentaire