Search code examples
rustownershipborrowing

How do I mutate in a match which borrows an immutable value?


I can understand borrowing/ownership concepts in Rust, but I have no idea how to work around this case:

use std::collections::{HashMap, HashSet};

struct Val {
    t: HashMap<u16, u16>,
    l: HashSet<u16>,
}

impl Val {
    fn new() -> Val {
        Val {
            t: HashMap::new(),
            l: HashSet::new(),
        }
    }

    fn set(&mut self, k: u16, v: u16) {
        self.t.insert(k, v);
        self.l.insert(v);
    }

    fn remove(&mut self, v: &u16) -> bool {
        self.l.remove(v)
    }

    fn do_work(&mut self, v: u16) -> bool {
        match self.t.get(&v) {
            None => false,
            Some(r) => self.remove(r),
        }
    }
}

fn main() {
    let mut v = Val::new();

    v.set(123, 100);
    v.set(100, 1234);

    println!("Size before: {}", v.l.len());
    println!("Work: {}", v.do_work(123));
    println!("Size after: {}", v.l.len());
}

playground

The compiler has the error:

error[E0502]: cannot borrow `*self` as mutable because it is also borrowed as immutable
  --> src/main.rs:28:24
   |
26 |         match self.t.get(&v) {
   |               ------ immutable borrow occurs here
27 |             None => false,
28 |             Some(r) => self.remove(r),
   |                        ^^^^^------^^^
   |                        |    |
   |                        |    immutable borrow later used by call
   |                        mutable borrow occurs here

I don't understand why I can't mutate in the match arm when I did a get (read value) before; the self.t.get is finished when the mutation via remove begins.

Is this due to scope of the result (Option<&u16>) returned by the get? It's true that the lifetime of the result has a scope inside the match expression, but this design-pattern is used very often (mutate in a match expression).

How do I work around the error?


Solution

  • The declaration of function HashMap::<K,V>::get() is, a bit simplified:

    pub fn get<'s>(&'s self, k: &K) -> Option<&'s V>
    

    This means that it returns an optional reference to the contained value, not the value itself. Since the returned reference points to a value inside the map, it actually borrows the map, that is, you cannot mutate the map while this reference exists. This restriction is there to protect you, what would happen if you remove this value while the reference is still alive?

    So when you write:

    match self.t.get(&v) {
        None => false,
        //r: &u16
        Some(r) => self.remove(r)
    }
    

    the captured r is of type &u16 and its lifetime is that of self.t, that is, it is borrowing it. Thus you cannot get a mutable reference to self, that is needed to call remove.

    The simplest solution for your problem is the clone() solves every lifetime issue pattern. Since your values are of type u16, that is Copy, it is actually trivial:

    match self.t.get(&v) {
        None => false,
        //r: u16
        Some(&r) => self.remove(&r)
    }
    

    Now r is actually of type u16 so it borrows nothing and you can mutate self at will.

    If your key/value types weren't Copy you could try and clone them, if you are willing to pay for that. If not, there is still another option as your remove() function does not modify the HashMap but an unrelated HashSet. You can still mutate that set if you take care not to reborrow self:

        fn remove2(v: &u16, l: &mut HashSet<u16>) -> bool {
            l.remove(v)
        }
        fn do_work(&mut self, v: u16) -> bool {
            match self.t.get(&v) {
                None => false,
                //selt.t is borrowed, now we mut-borrow self.l, no problem
                Some(r) => Self::remove2(r, &mut self.l)
            }
        }