jeudi 24 mars 2022

Satisfying the borrow checker with a struct that processes a queue of actions

I am trying to write a struct which owns some data in a Vec (or perhaps contains a Vec of mutable references - not really important which one), and which can process a queue of "actions", where each action is some sort of calculation which mutates the elements of this Vec. Here is a minimal example of what I have written so far:

// some arbitrary data - may be large, so should not be cloned or copied
#[derive(PartialEq)]
struct T(i32, &'static str);

struct S(Vec<T>);
impl S {
    fn get_mut(&mut self, t: &T) -> &mut T {
        self.0.iter_mut().find(|a| *a == t).unwrap()
    }
    fn process_actions(&mut self, queue: ActionQueue) {
        // some arbitrary calculation on the elements of self.0
        for a in queue.actions {
            let t1 = self.get_mut(a.t1);
            t1.0 += a.t2.0;
        }
    }
}

#[derive(Debug)]
enum Error {
    ActionError,
    ActionQueueError,
}

struct Action<'a> {
    s: &'a S,
    t1: &'a T,
    t2: &'a T,
}
impl<'a> Action<'a> {
    pub fn new(s: &'a S, t1: &'a T, t2: &'a T) -> Result<Action<'a>, Error> {
        if s.0.contains(&t1) && s.0.contains(&t2) {
            Ok(Action { s, t1, t2 })
        } else {
            Err(Error::ActionError)
        }
    }
}

struct ActionQueue<'a> {
    s: &'a S,
    actions: Vec<Action<'a>>,
}
impl<'a> ActionQueue<'a> {
    pub fn new(s: &'a S, actions: Vec<Action<'a>>) -> Result<ActionQueue<'a>, Error> {
        if actions.iter().all(|a| std::ptr::eq(a.s, s)) {
            Ok(ActionQueue { s, actions })
        } else {
            Err(Error::ActionQueueError)
        }
    }
}

fn main() -> Result<(), Error> {
    let t1 = T(1, "a");
    let t2 = T(2, "b");
    let mut s = S(vec![t1, t2]);

    let a = Action::new(&s, &t1, &t2)?; // error: borrow of moved value: `t1`
    let q = ActionQueue::new(&s, vec![a])?;
    s.process_actions(q); // cannot borrow `s` as mutable because it is also borrowed as immutable

    Ok(())
}

There are a few problems with this:

  1. I can't create the Action because t1 and t2 have already been moved.
  2. Even if I could, I can't process the action queue because s is already immutably borrowed in the Action. The reason I want the Action (and ActionQueue) to contain a reference to s is because, from what I understand, it's good to use types prevent the creation of invalid data, e.g. an Action (to be processed by s) referring to data not contained in s.
  3. The get_mut function of S seems a bit weird and hacky, like I shouldn't need to have such a function.

I understand why the errors occur and what they mean, but I don't see any way to get around this problem, because in order to define any Action, I need to refer to elements of s.0 which I am not allowed to do. So my question is, how should this code be rewritten so that it will actually compile? It doesn't matter if the design is completely different, as long as it achieves the same goal (i.e. allows to queue up actions to be processed later).

Aucun commentaire:

Enregistrer un commentaire