Do lifetimes determine mutability?

Looking at this example:

use std::collections::HashMap;

struct Foo<'a> {
    lookup: HashMap<&'a str, String>,
}


impl<'a> Foo<'a> {
    fn new() -> Self {
        Self{ lookup: HashMap::new() }
    }
    
    //Added 'b for clarity
    fn calc<'b>(&'b mut self, key: &'a str) -> &'b str 
    {
        if let Some(s) = self.lookup.get(key) {
            return s.as_str()
        } else {
            self.lookup.insert(key, "secret to life, the universe, and everything".to_owned());
            self.lookup.get(key).unwrap().as_str()
        } 
    }
}

fn main() {
    let mut foo = Foo::new();
    
    println!("{}", foo.calc("42"));
}

The error is very descriptive:

14 |     fn calc<'b>(&'b mut self, key: &'a str) -> &'b str 
   |             -- lifetime `'b` defined here
15 |     {
16 |         if let Some(s) = self.lookup.get(key) {
   |                          ----------- immutable borrow occurs here
17 |             return s.as_str()
   |                    ---------- returning this value requires that `self.lookup` is borrowed for `'b`
18 |         } else {
19 |             self.lookup.insert(key, "secret to life, the universe, and everything".to_owned());
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here

First of all, in trying to understand this, I’m thinking that it’s saying something like the following:

  1. the return has a lifetime of 'b
  2. therefore even though return s happens in its own scope, it’s really bound by the entire lifetime of 'b
  3. since s is the result of self.lookup.get(), that means it the call to lookup.get() must be considered for the entire lifetime of 'b
  4. and so we can’t have lookup be both mutable and immutable borrows - i.e. both lookup.get() and lookup.insert() in this same function - even if we surround them with different blocks, because the lifetime is determined from the return.

Is that correct?

Now, in terms of a fix - it seems this requires RefCell or Cell?

In other words it looks very similar to this section: https://doc.rust-lang.org/std/cell/#implementation-details-of-logically-immutable-methods

I think your problem is similar to this issue (there are a few of them) apparently it will work with NLL v2.

But for your problem specifically you can use hashmap.entry() like this.

2 Likes

Whoa… lol, so I just made up that whole justification, trying to learn from the compiler…

:sweat_smile:

What’s the recommended way to deal with that where I also want to handle errors?

Here’s what I put together, but it’s a bit clunky: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=80b4ca1f08df284c02b527ce6ba2a516

specifically, as noted in the comments, it would be nicer to just use e.get() rather than reach back out to entry

That’s how you would do it, note that using the entry api also only hashes the key once, which can be nice when it is expensive to hash keys.

Note that because you are explicitly matching, you could use the inner entries instead like so
https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=8493e157e6458998f59f56356116da7d

1 Like

Hmmm that’s what I was trying to do with get()

I see now into_mut() ties the reference to the lifetime of the HashMap rather than the Entry… interesting!

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.