0

I’m trying to do some memoization in Rust and I’m running afoul of the borrow checker.

fn calc_deps(cache: &mut HashMap<String, String>, key: &String) -> String {
    if let Some(v) = cache.get(key) {
        v
    } else {
        let r = /* computations */
        cache.insert(key.clone(),r.clone());
        r
    }
}

I’m told that cache is borrowed twice. Why is it a problem if I’m done with get by the time I get to insert? Is there a way for me to encode that information?

trent
  • 25,033
  • 7
  • 51
  • 90
simon1505475
  • 675
  • 3
  • 9
  • 2
    If I [make edits to your code so that I get the borrowing error](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2015&gist=7afd32c77449323f34ca70007af64837), it compiles on nightly with `#![feature(nll)]`. See [What are non-lexical lifetimes?](https://stackoverflow.com/questions/50251487/what-are-non-lexical-lifetimes) – trent Nov 04 '18 at 13:11
  • 2
    Also tangentially relevant: [Why is it discouraged to accept a reference to a String (&String), Vec (&Vec) or Box (&Box) as a function argument?](https://stackoverflow.com/questions/40006219/why-is-it-discouraged-to-accept-a-reference-to-a-string-string-vec-vec-or) – trent Nov 04 '18 at 13:13

1 Answers1

3

The problem is that the lifetime of v is for the whole if/else block, even though it is not available in the else section. You can get around this by with the help of Option::cloned.

pub fn calc_deps(cache: &mut HashMap<String, String>, key: &String) -> String {
    if let Some(v) = cache.get(key).cloned() {
        v
    } else {
        let r = String::from("computations");
        cache.insert(key.clone(), r.clone());
        r
    }
}

Option::cloned maps Option<&T> to Option<T> by cloning the contents. So now v becomes String instead of &String, and is no longer borrowing cache.

Another option is to use the HashMap::entry/or_insert_with interface. It's probably more idiomatic, but it requires unconditionally cloning the key.

pub fn calc_deps(cache: &mut HashMap<String, String>, key: String) -> String {
    cache
        .entry(key)
        .or_insert_with(|| String::from("computations"))
        .clone()
}

You could also simply use or_insert instead of or_insert_with, but that would require doing your computations for r unconditionally.

pub fn calc_deps(cache: &mut HashMap<String, String>, key: String) -> String {
    cache
        .entry(key)
        .or_insert(String::from("computations"))
        .clone()
}
Benjamin Lindley
  • 101,917
  • 9
  • 204
  • 274
  • I like these solutions. What if I wanted to stick to the initial idea to basically implement the functions you showed us here? Shouldn't I be able to write something like: ``` { let x; { x = cache.get(key).clone() }; if let Some(v) = x { ... ``` Why doesn't that work? – simon1505475 Nov 05 '18 at 02:09
  • 1
    @simon1505475: Because when you clone an `Option<&String>`, you still have an `Option<&String>`. That's why I used `map` to take the reference out, and then I cloned it, because calling `clone` on a reference clones the referred object. However, I just learned about another function for `Option` that does what I did with `map`. `if let Some(v) = cache.get(key).cloned() { ...`. (not that it's `cloned` instead of `clone`) I'll be updating my answer to reflect it. – Benjamin Lindley Nov 05 '18 at 03:39