Search code examples
rustborrow-checker

Rust: return immutable borrow after modifying


In Rust I am trying to update a cache and then return the result.

fn cached_res(&mut self) -> &Vec<u64> {
  if self.last_update.elapsed() >= REFRESH_RATE {
    self.a = do_something(...);
  }
  return &self.a;
}

fn use_cached_res(&mut self) {
  let b = self.cached_res();
  b.iter().map(|x| x + 1).collect()
}

And the compilation fails due to

173 |         let b = cached_res(self);
    |                            ---- mutable borrow occurs here
...
179 |                 .map(|f| self.do_something(&f))
    |                  --- ^^^ ---- second borrow occurs due to use of `*self` in closure
    |                  |   |
    |                  |   immutable borrow occurs here
    |                  mutable borrow later used by call

I think I understand why it fails and a possible workaround could be to have a method that only updates the cache and another that returns the result so in that way the mutable borrow is dropped and b is not borrowed as mutable.

fn refresh(&mut self) {
  if self.last_update.elapsed() >= REFRESH_RATE {
    self.a = do_something(...);
  }
}
fn res(&self) -> &Vec<u64> {
  &self.a
}
fn use_cached_res(&mut self) {
  self.refresh();
  let b = self.res();
  b.iter().map(|x| x + 1).collect()
}

However, I would like to have these constraints defined in cached_res so that downstream users don't have to use such methods in the right sequence. IIUC what I want is that cached_res returns an immutable borrow of self after having modified self.

Please let me know if there is additional information or clarifications that I can provide.


Solution

  • One way to solve it is with interior mutability using RefCell:

    use std::cell::RefCell;
    use std::ops::Deref;
    use std::time::{Duration, Instant};
    
    const REFRESH_RATE: Duration = Duration::from_millis(16);
    
    pub struct A {
        a: RefCell<Vec<u64>>,
        last_update: Instant,
    }
    
    impl A {
        fn cached_res(&self) -> impl Deref<Target = Vec<u64>> + '_ {
            if self.last_update.elapsed() >= REFRESH_RATE {
                *self.a.borrow_mut() = Vec::new();
            }
            self.a.borrow()
        }
    
        pub fn use_cached_res(&self) -> Vec<u64> {
            let b = self.cached_res();
            b.iter().map(|&x| self.process(x)).collect()
        }
    
        fn process(&self, _x: u64) -> u64 {
            todo!()
        }
    }
    

    This has some extra overhead to keep track of borrows at runtime, but will give you the most flexibility and would even allow you to return impl DerefMut if you like. You can easily translate this to RwLock (borrow_mut -> write and borrow -> read) or Mutex (lock) if you need this struct to be Sync.

    Another way is to invert the control flow by taking a closure:

    use std::time::{Duration, Instant};
    
    const REFRESH_RATE: Duration = Duration::from_millis(16);
    
    pub struct A {
        a: Vec<u64>,
        last_update: Instant,
    }
    
    impl A {
        fn cached_res<F, R>(&mut self, f: F) -> R
        where
            F: FnOnce(&Self, &Vec<u64>) -> R,
        {
            if self.last_update.elapsed() >= REFRESH_RATE {
                self.a = Vec::new();
            }
            f(self, &self.a)
        }
    
        pub fn use_cached_res(&mut self) -> Vec<u64> {
            self.cached_res(|self_, b| b.iter().map(|&x| self_.process(x)).collect())
        }
    
        fn process(&self, _x: u64) -> u64 {
            todo!()
        }
    }
    

    This puts constraints on how you can use cached_res but doesn't require changing your struct or introducing overhead.