Multiple mutable methods

Heres one way to see how lifetime analysis is working.

First the definition of Foo

struct Foo<'a> {
    lookup: HashMap<u32, Bar<'a>>,
    current: Option<&a' Bar<'a>>
}

Here we are saying that the lifetime in current is the same as in lookup. Simple stuff, binding lifetimes together.

Next, in Foo::new

    ...
    fn new() -> Self {
        let mut lookup = HashMap::new();
        lookup.insert(42, Bar{message: "secret to life, the universe, and everything"});
        
        Self{
            lookup,
            current: None
        }
    }
    ...

Because string literals have the type &'static str, they subtype &'a str. So it is safe to use &'static str where &'a str is expected (in lookup). So now you can create an arbitrary Foo<'a>.

Finally on to calc

    fn calc(&mut self) -> &str {
        if self.current.is_none() {
            self.current = Some(self.lookup.get(&42).unwrap());
        }
        self.current.unwrap().message
    }

Here the important thing to note is the assignment to current. This is important because the type of current is &'a Bar<'a> (remember this, as it will be important shortly).

Also note that we have self behind a &mut, this is important because &mut T is invariant over T. What this means for lifetimes is that the &mut &'a T is only equal to &mut &'b T if and only if 'a == 'b (ignoring the lifetimes associated with the &'0 mut, labeled '0 here).

What this means for current is that, this assignment will determine the lifetime 'a.

Now, that we got that, let's desugar calc.

    fn calc<'b>(&'b mut self) -> &'b str {
        if self.current.is_none() {
            self.current = Some(self.lookup.get(&42).unwrap());
        }
        self.current.unwrap().message
    }

We now have some other lifetime 'b. Now let's examine the assignment. we are assigning to current, what are we assigning. If we look at the RHS we see, Some(self.lookup.get(&42).unwrap()), which is of the type Option<&'b Bar<'a>>. But we are trying to assign it to the LHS, current, which has the type Option<&'a Bar<'a>>.

Oh no! What if 'a: 'b, and 'a != 'b. This would mean that this assignment is invaid! It would produce a dangling pointer. Hence why &mut T is invariant over T. Because of this, Rust checks that 'a == 'b, but it can't enforce this in general because you could pick 'b to be any lifetime that you want.

So if we correct this signature, and enforce that &'a mut self, then 'a == 'b, and everything works out.

So

    fn calc(&'a mut self) -> &'a str {
        if self.current.is_none() {
            self.current = Some(self.lookup.get(&42).unwrap());
        }
        self.current.unwrap().message
    }

works. Now you may notice that I added a lifetime to &'a str. Rust does this desugaring automatically, it is called lifetime elision, and it makes working with lifetimes painless in the normal case, and it is what your lifetime annotated code desugars to :slight_smile: .

Note that this also works

    fn calc<'b>(&'b mut self) -> &'b str
    where 'b: 'a {
        if self.current.is_none() {
            self.current = Some(self.lookup.get(&42).unwrap());
        }
        self.current.unwrap().message
    }

I will leave this to you to figure out why.


edited because I was wrong in my previous atempt at this post, lifetimes are hard and I missed the actual problem with the code. I aslo didn't consider variance so that led me astray. But this is the correct reason for why calc didn't compile without annotation.

edit 2: formatting and wording

8 Likes