0

I have a cache-like structure which internally uses a HashMap:

impl Cache {
    fn insert(&mut self, k: u32, v: String) {
        self.map.insert(k, v);
    }

    fn borrow(&self, k: u32) -> Option<&String> {
        self.map.get(&k)
    }
}

Playground with external mutability

Now I need internal mutability. Since HashMap does not implement Copy, my guess is that RefCell is the path to follow. Writing the insert method is straight forward but I encountered problems with the borrow-function. I could return a Ref<String>, but since I'd like to cache the result, I wrote a small Ref-wrapper:

struct CacheRef<'a> {
    borrow: Ref<'a, HashMap<u32, String>>,
    value:  &'a String,
}

This won't work since value references borrow, so the struct can't be constructed. I know that the reference is always valid: The map can't be mutated, because Ref locks the map. Is it safe to use a raw pointer instead of a reference?

struct CacheRef<'a> {
    borrow: Ref<'a, HashMap<u32, String>>,
    value:  *const String,
}

Am I overlooking something here? Are there better (or faster) options? I'm trying to avoid RefCell due to the runtime overhead.

Playground with internal mutability

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Tim Diekmann
  • 7,755
  • 11
  • 41
  • 69
  • *I wrote a small `Ref`-wrapper* — exactly what benefit to seek to gain by writing such a wrapper? – Shepmaster Mar 19 '18 at 13:42
  • With the wrapper, I can search the key in the map and store the Result. Dereferencing `Ref` would result in `&HashMap`, dereferencing the wrapper results in `&T` – Tim Diekmann Mar 19 '18 at 13:44
  • 1
    `Ref` has a `map` associated function that *should* be used to translate the reference to the whole map into a reference to one element. But its signature is not flexible enough; I can't make it work with the `Option` wrapper. – Sebastian Redl Mar 19 '18 at 13:47
  • @SebastianRedl yeah, [I'm stuck as well](https://play.rust-lang.org/?gist=9f9cf1f85d18ec82a5153ea363043487&version=stable). – Shepmaster Mar 19 '18 at 13:53

3 Answers3

2

I'll complement @Shepmaster's safe but not quite as efficient answer with the unsafe version. For this, we'll pack some unsafe code in a utility function.

fn map_option<'a, T, F, U>(r: Ref<'a, T>, f: F) -> Option<Ref<'a, U>>
where
    F: FnOnce(&'a T) -> Option<&'a U>
{
    let stolen = r.deref() as *const T;
    let ur = f(unsafe { &*stolen }).map(|sr| sr as *const U);
    match ur {
        Some(u) => Some(Ref::map(r, |_| unsafe { &*u })),
        None => None
    }
}

I'm pretty sure this code is correct. Although the compiler is rather unhappy with the lifetimes, they work out. We just have to inject some raw pointers to make the compiler shut up.

With this, the implementation of borrow becomes trivial:

fn borrow<'a>(&'a self, k: u32) -> Option<Ref<'a, String>> {
    map_option(self.map.borrow(), |m| m.get(&k))
}

Updated playground link

The utility function only works for Option<&T>. Other containers (such as Result) would require their own modified copy, or else GATs or HKTs to implement generically.

Sebastian Redl
  • 69,373
  • 8
  • 123
  • 157
  • I think this suites my needs the most. I updated the [Playground](https://play.rust-lang.org/?gist=9a43346fc5bd3685fc58d89cb4ee5633&version=stable) and added a mutable version as well. – Tim Diekmann Mar 20 '18 at 08:39
1

I'm going to ignore your direct question in favor of a definitely safe alternative:

impl Cache {
    fn insert(&self, k: u32, v: String) {
        self.map.borrow_mut().insert(k, v);
    }

    fn borrow<'a>(&'a self, k: u32) -> Option<Ref<'a, String>> {
        let borrow = self.map.borrow();

        if borrow.contains_key(&k) {        
            Some(Ref::map(borrow, |hm| {
                hm.get(&k).unwrap()
            }))
        } else {
            None
        }
    }
}

Ref::map allows you to take a Ref<'a, T> and convert it into a Ref<'a, U>. The ugly part of this solution is that we have to lookup in the hashmap twice because I can't figure out how to make the ideal solution work:

Ref::map(borrow, |hm| {
    hm.get(&k) // Returns an `Option`, not a `&...`
})

This might require Generic Associated Types (GATs) and even then the return type might be a Ref<Option<T>>.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
1

As mentioned by Shepmaster, it is better to avoid unsafe when possible.

There are multiple possibilities:

  • Ref::map, with double look-up (as illustrated by Shepmaster's answer),
  • Ref::map with sentinel value,
  • Cloning the return value.

Personally, I'd consider the latter first. Store Rc<String> into your map and your method can easily return a Option<Rc<String>> which completely sidesteps the issues:

fn get(&self, k: u32) -> Option<Rc<String>> {
    self.map.borrow().get(&k).cloned()
}

As a bonus, your cache is not "locked" any longer while you use the result.

Or, alternatively, you can work-around the fact that Ref::map does not like Option by using a sentinel value:

fn borrow<'a>(&'a self, k: u32) -> Ref<'a, str> {
    let borrow = self.map.borrow();

    Ref::map(borrow, |map| map.get(&k).map(|s| &s[..]).unwrap_or(""))
}
Matthieu M.
  • 287,565
  • 48
  • 449
  • 722