Fighting the borrow checker, in a loop

Same as the one for &mut self - take a look at lifetime elision rules for more info.

Why?

Which part? :slight_smile:

I expect this one doesn't compile.
I know the reason why it doesn't compile is that &mut self should live at least as long as 'a.
But what surprises me is this compiles.

    fn add_child(&mut self, child:Node<'a>) -> &'a Node{
        self.children.push(Box::<Node>::new(child));
        self
    }

Shouldn't this be de-sugared to exactly the other one ?

It desugars to:

fn add_child<'b>(&'b mut self, child:Node<'a>) -> &'a Node

There’s no (outlives) relationship between 'a and 'b there beyond the implied one - 'b is shorter than 'a.

As I mentioned earlier, it's rust 2018 edition, which means that it has non-lexical-lifetimes which are more lax in certain situations (Which I think includes this one)

<'a, 'b: 'a>

Means that 'a is a lifetime, and that 'b is another lifetime that is greater than or equal to 'a
"Desugars" refers to 'it is equivalent to in the eyes of the compiler'

Ah, I see, Thanks a lot :slight_smile:

but why this works Rust Playground

impl <'a> Node<'a> {
    fn add_child<'b>(&'b mut self, child:Node<'a>) ->  &'a mut Node<'b>{
        self.children.push(Box::<Node>::new(child));
        self
    }
}

But this one doesn't

impl <'a> Node<'a> {
    fn add_child<'b>(&'b mut self, child:Node<'a>) ->  &'a mut Node<'a>{
        self.children.push(Box::<Node>::new(child));
        self
    }
}

And also what is &'a mut Node<'b>, sounds like a reference lives longer than the Node itself ?

This is very deep, and I am drowning.
But...

Outside of the test case that I built to explain the error and in the lib I am building I had the following:

    fn add_child(& mut self, child:Node<'a>) ->  &'a  mut Node{

Which did not compile.

    fn add_child<'b>(&'b mut self, child:Node<'a>) ->  &'a  mut Node{

does.

I have read everything I can get my hands on about life times and I am way out of my depth! As far as I can tell it is throwing paint at a wall...
Is there and literature around rust similar to what Bjarne Stroustrup wrote for C++? I found " The Design and Evolution of C++" really helpful. It would be really helpful for Rust.

I fear that I will not be able to use Rust for anything useful as I am still having problems like these that I have to solve by using random invocations that I do not understand.

(Some of this is presumptions, I'm not the best at lifetimes...)

In this case, it is implied that 'a has to be greater than or equal to 'b (So that a 'b can contain it), in which case, &'b mut self refers to a self which has to live shorter than or equal to 'a, but we attempt to pass it off as 'a, which may be larger than 'b
Edit:
Yeah, vitalyd's post explains it better

But if this is the case how could &'a mut Node<'b> meaningful if 'b lives shorter than 'a

I am not confused about this one, but I am confused about the other one, since I expect the both case should fail.

This is also true in another case, because the self only has a lifetime of 'b. But it compiles when the return type is &'a mut Node<'b>. (Note the lifetime of the returning reference doesn't change)

When you write add_child<'b>(&'b mut self, ...), the compiler selects the right 'b when it calls this method. In this case, it's as-if you wrote:

fn add_child<'b: 'a>(&'b mut self, child:Node<'a>) ->  &'a mut Node<'b>{
        self.children.push(Box::<Node>::new(child));
        self
    }

Prior to NLL, this would prevent you from calling add_child again because the mutable borrow is extended beyond the lifetime of the Node value. With NLL, the compiler can see that the returned reference isn't even used, and so allows the calling code (i.e. the loop) to proceed. Note that the pre-NLL error is at the call/use site.

This is now error at definition site - the signature and body of the method don't align: given a self borrow for 'b, you can't return self as-if it's &'a Node<'a> - you'd need to borrow self for 'a in that case, explicitly in the signature.

Edit: correcting illegible stuff I wrote - responding on mobile is hard :slight_smile:

2 Likes

Ah I see, thanks for the explain. That's really helpful. :slight_smile:

One more question. But why if I write 'b explicitly it prevents me from calling the function again ?

impl <'a> Node<'a> {
    fn add_child<'b:'a>(&'b mut self, child:Node<'a>) ->  &'a mut Node<'b>{
        self.children.push(Box::<Node>::new(child));
        self
    }
}

Isn't the compiler detect the return value isn't used as well ?

What is the colon for in <'b:'a>?

'a is greater than or equal to 'b

<'b:'a>

I am really confused! So 'a >= 'b means that 'b contains 'a?

{
    let a = scope1;
    {
        let b = scope2;
    }
}

Do we say scope1 contains scope2? (I would)

But then I would say scope1 >= scope2.

Oops, sorry, I meant to say that so 'b can be contained within it :sweat_smile:

This gets a bit subtle :slight_smile:.

In the elision case (or when you don’t express the outlives relationship explicitly), the compiler knows you can’t take advantage of 'b: 'a inside the body of the method - it’s only at the callsite that it makes the lifetimes work out.

In the explicit case you have above, it’s like a “hard requirement”, if you will - you could now code the body of that method knowing that 'b: 'a (eg stash something away with 'b lifetime into an 'a, knowing that b outlives a). The compiler can no longer play games with the lifetimes.

2 Likes

I see, since the function with different lifetime param actually shares code. So the compiler doesn't realize 'b outlives 'a when compiles the function, until the function is called. Is that the right explain?
So this makes ellision semantics slight different than just syntax sugar ?

Elision should be no different than writing the lifetimes explicitly assuming you write out the same lifetimes and their bounds as elision rules. In other words, elision is purely about ergonomics, not unlocking some additional capabilities.

There are basically two places where lifetimes come into play in the examples here: declaration and use/callsite. The compiler checks them separately - a function declaration and body are checked to make sure they’re valid on their own, without any particular caller. At the use/callsite, the compiler checks the lifetimes just against the signature of the callee - it ensures the caller abides by the lifetime requirements of the callee. And so there are multiple opportunities to get something wrong here, independent of each other :slight_smile:.

NLL mostly, if not entirely, addresses the latter part: improving the uses/callsites, such as by observing that a returned reference is dropped immediately or otherwise dies between loop iterations, as in this thread’s subject.

1 Like