How to borrow a HashMap mutably in a function invocation inside a closure?

I'm trying to use cache mutably in the invalidate_backtrace invocation in the closure:

use hotwatch::{
    blocking::{Flow, Hotwatch},
    Event,
};
use std::collections::HashMap;
use std::path::PathBuf;

fn invalidate_backtrace(cache: HashMap<PathBuf, usize>, path: PathBuf) {
    let path = &mut path.clone();
    while path.parent() != None {
        cache.remove(path);
        path.pop();
    }
}

fn main() {
    let args = std::env::args();
    if args.len() == 0 {
        return;
    }

    let mut cache = HashMap::<PathBuf, usize>::new();
    let mut watcher = Hotwatch::new().expect("hotwatch failed to initialize!");
    for path in args.skip(1) {
        watcher
            .watch(path, |event: Event| {
                match event {
                    Event::NoticeWrite(file_path) => invalidate_backtrace(cache, file_path),
                    _ => {}
                };
                Flow::Continue
            })
            .expect("failed to watch file!");
    }
    watcher.run();
}

However, I simply cannot get the code to compile. I've tried to use various combinations of mutable references, RefCell, Rc and Arc with the cache value, to no avail.

(NB: watcher.run() is a blocking invocation which (AFAIK) does not spawn another thread.)

The difficulty lies in the fact that we want to give out mutable shared access to cache, since every closure needs to access it, but the closures are stored inside the watcher, and as such all the closures exist at the same time inside run.

To get around this, I recommend defining at type like the following:

use std::rc::Rc;
use std::cell::RefCell;

#[derive(Clone)]
struct Cache {
    inner: Rc<RefCell<HashMap<PathBuf, usize>>>,
}
impl Cache {
    pub fn new() -> Self {
        Self {
            inner: Rc::new(RefCell::new(HashMap::new())),
        }
    }

    pub fn remove(&self, path: &Path) {
        let mut inner = self.inner.borrow_mut();
        inner.remove(path);
    }
}

This type derives clone, but every clone of it yields a new shared handle to the same hash map, and changes to any clone of it are visible in any other clone.

Generally I find that code like this is easiest to read if you only call the borrow_mut() method inside methods on the struct, like I have done with the remove method. Calling .borrow_mut() in code that uses the cache clutters it and makes it hard to read.

Full example
use hotwatch::{
    blocking::{Flow, Hotwatch},
    Event,
};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::cell::RefCell;

#[derive(Clone)]
struct Cache {
    inner: Rc<RefCell<HashMap<PathBuf, usize>>>,
}
impl Cache {
    pub fn new() -> Self {
        Self {
            inner: Rc::new(RefCell::new(HashMap::new())),
        }
    }

    pub fn remove(&self, path: &Path) {
        let mut inner = self.inner.borrow_mut();
        inner.remove(path);
    }
}

fn invalidate_backtrace(cache: &Cache, path: PathBuf) {
    let path = &mut path.clone();
    while path.parent() != None {
        cache.remove(path);
        path.pop();
    }
}

fn main() {
    let args = std::env::args();
    if args.len() == 0 {
        return;
    }

    let cache = Cache::new();
    let mut watcher = Hotwatch::new().expect("hotwatch failed to initialize!");
    for path in args.skip(1) {
        let cache = cache.clone();
        watcher
            .watch(path, move |event: Event| {
                match event {
                    Event::NoticeWrite(file_path) => invalidate_backtrace(&cache, file_path),
                    _ => {}
                };
                Flow::Continue
            })
            .expect("failed to watch file!");
    }
    watcher.run();
}

The purpose of the Rc is to make the access shared. Without the Rc, the clones would not access the same underlying hash map.

The purpose of RefCell is to allow mutation through shared access. Without the RefCell, you would only have immutable access to the map.

2 Likes

Thank you, this solved my problem!