While let borrow lifetime seems to be too long

Hello,
By my reasoning, the two while let -loop headers in the program below should be equivalent, but the commented-out one causes a panic, as the borrowed value is not dropped by the time control enters the loop body. Or am I missing something here?

This can be worked around by introducing an explicit inner let block, as shown in the code.

Is there some reason to keep the value alive so long?

After some googling, this seems to be connected to Non-Lexical Lifetimes development. Would this be changed as that work is complete?

use std::cell::RefCell;
fn main() {
  let v = RefCell::new(vec![1,2,3]);
  //while let Some(i) = v.borrow_mut().pop() {  // this panics with BorrowMutError
  while let Some(i) = {let mut vv = v.borrow_mut(); vv.pop()} { // inner let is a workaround for the panic
    println!("head was {:?}, next is {:?}", i , v.borrow_mut().pop());
  }
}

(Playground)

Output:

head was 3, next is Some(2)
head was 1, next is None

inside the while let closure, you're borrowing the mut again. This compiles fine:

use std::cell::RefCell;
fn main() {
  let v = RefCell::new(vec![1,2,3]);
  let mut borrow = v.borrow_mut();
  while let Some(i) = borrow.pop() {
    println!("next is {:?}", i);
  }
}

Borrow the mut once, then don't borrow it again until you've dropped it via std::mem::drop(borrow) (or, exit the calling closure)

Ah, it seems that I over-minimized my example, as you have found a more elegant workaround for the simplified case.

My actual program is much more complicated, and the RefCell is really inside Rc, which is of course being shared to another data structure.

Another attempt at minimal example program:

use std::cell::RefCell;
use std::rc::Rc;

fn main() {
  let v = Rc::new(RefCell::new(vec![1,2,3]));
  
  // This simulates the other Rc reference
  let other_ref = v.clone();
  let function_from_elsewhere = move || other_ref.borrow_mut().pop();
  
  //while let Some(i) = v.borrow_mut().pop() {  // this panics with BorrowMutError
  while let Some(i) = {let mut vv = v.borrow_mut(); vv.pop()} { // inner let is a workaround for the panic
    println!("head was {:?}, next is {:?}", i , function_from_elsewhere() );
  }
}

So the original question still stands: Should both variants of the while-loop work?

It looks like the RefMut is kept alive longer than expected because of some variation of temporary lifetime extension. However, it seems temporaries in the matched expression of while let and match do not follow the same rules as other loops and conditionals such as if let or if or while.

If I understand correctly, temporaries in these matched expressions are always kept alive until the end of the entire while let or match block, even in cases where it should not be required.

This behavior is not documented or specified anywhere, as far as I can tell. There was an issue filed about this a long time ago, and more recently this documentation issue and this forum thread.

2 Likes

Yes, this is the main difference between match (and derived forms of it, such as if let and while let), vs. let assignments.

Indeed, when a temporary is created inside an expression, it is (only) dropped at the end of the enscoping statement.

  • A let binding ends its statement before the binding ("variable") is usable;

  • A match and derivatives, end the statement after the scope in which the binding ("variable") is usable.

Indeed, let's compare

while let Some(it) = { let elem = v.borrow_mut().pop(); elem }
{ ... }

with:

while let Some(it) = { v.borrow_mut().pop() }
{ ... }

Contrary to what many people think, there is a huge difference between these two.

We have:

while let Some(it) = {
    let mut elem = //            |
    //     temporary             |
    //  --------------           | enscoping statement
        v.borrow_mut().pop() //  |
    ; //                         |
    // <- temporary dropped here: *borrow guard gets released*
    elem
}
{
    // body: can requery the borrow.
}

vs.

while let Some(it) = {                   // |
//     temporary                            |
//  --------------                          |
    v.borrow_mut().pop()                 // |
}                                        // |
{                                        // | enscoping statement
    // body: cannot requery the borrow!     |
}                                        // |
// ;                                        |
// <- temporary dropped here: *borrow guard gets released*

The rule of thumb is to remember that these "statements" thus have a terminanting semi-colon ; even when elided, and so, the rule becomes:

Temporaries are only dropped / cleaned up at the end of their enscoping statement, that is, "when they encounter a ;" (at the same "depth" they were created).

This is usually not a problem, except when dealing with guards.


FWIW, this means that one can easily create the following macro:

macro_rules! drop_guards {( $expr:expr ) => (
    { let it = $expr; it }
)}

and then do:

while let Some(elem) = drop_guards!(v.borrow_mut().pop()) {
    // body
}
2 Likes

It's a little more complicated that that, however, since temporaries in the conditional of an if or if let or while expression can be dropped before the end of the enclosing statement, but not in a while let or match expression. Unfortunately, as you concluded in the previous thread, making this more consistent would break existing code.

3 Likes

Thanks for pointing that out, I had forgotten that post of mine :laughing: and those two observations: the inconsistency with if let, and the fact that changing that incurs in a semantic change that could cause unsoundness in code out there (worst form of breakage!).

There is a discussion in Zulip suggesting to use .move to perform, for instance, some_immutable_vec.move.pop() (instead of the current {some_immutable_vec}.pop()), and I'm thinking that enhacing .move to not only do { <expr> }, but to actually do { let it = <expr>; it } so as to drop temporaries, would offer a not-so-ugly solution for this problem:

while let Some(x) = v.borrow_mut().pop().move {
2 Likes

Thank you for the in-depth explanation. This (scope/lifetime rules) was something that is not very easy to find in the usual documentation.

It's all there in chapter 4 of the Rust book. Almost at the beginning : https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html

Although I suspect the discussion of stack, local variables, scopes, and hence lifetimes is a bit brief for those who have not come from a background in C/C++ or a bunch of other compiled programming languages. In those languages thinking about scopes/lifetimes is very important else things go horribly wrong. Even if the languages themselves have no syntax for describing lifetimes and don't enforce any safety rules at compile time.

Those who have only ever used interpreted/garbage collected languages like Python, Java, Javascript have likely never had to be aware of such things. And could perhaps do with a bit more hand holding in that chapter.

That chapter you are referring to is a very good introduction to the topic in general.

I meant a more reference-type documentation, which would explain the scope rules for various Rust constructs, including the thread topic here, namely temporary values generated in while let -expressions. Usually, I have found Rust scopes to work in a quite intuitive manner, but this one was a surprise.

Me too. I'm not surprised that you are surprised. I would never have dreamt of writing such a tortuously complex line as :

`while let Some(i) = {let mut vv = v.borrow_mut(); vv.pop()} {
...

Despite my year of Rust I would have been stopped dead by that wondering what does or even if what it does is actually what the author intended to do.

I mean, at this point I'd go for either defining a wrapper struct with the borrow_mut() inside a call to a method on that struct, or I would combine a loop with break rather than a while loop.

1 Like

Wow, Alice, I'm glad you said that first.

I was just looking at the code in the OP and thinking that starting with an infinite loop

loop {
    ...

And then spelling the rest of it out, one action per line, and putting a break in the appropriate place would be a hundred times clearer.

I dare not say that though. My passion for clear and simple loops usually results in a long debate with the functional style crowd.

1 Like