Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is a borrow still held in the else block of an if let?

Tags:

rust

Why does the call self.f2() in the following code trip the borrow checker? Isn't the else block in a different scope? This is quite a conundrum!

use std::str::Chars;

struct A;

impl A {
    fn f2(&mut self) {}

    fn f1(&mut self) -> Option<Chars> {
        None
    }

    fn f3(&mut self) {
        if let Some(x) = self.f1() {

        } else {
            self.f2()
        }
    }
}

fn main() {
    let mut a = A;
}

Playground

error[E0499]: cannot borrow `*self` as mutable more than once at a time
  --> src/main.rs:16:13
   |
13 |         if let Some(x) = self.f1() {
   |                          ---- first mutable borrow occurs here
...
16 |             self.f2()
   |             ^^^^ second mutable borrow occurs here
17 |         }
   |         - first borrow ends here

Doesn't the scope of the borrow for self begin and end with the self.f1() call? Once the call from f1() has returned f1() is not using self anymore hence the borrow checker should not have any problem with the second borrow. Note the following code fails too...

// ...
if let Some(x) = self.f1() {
    self.f2()
}
// ...

Playground

I think the second borrow should be fine here since f1 and f3 are not using self at the same time as f2.

like image 391
Autodidact Avatar asked May 14 '15 17:05

Autodidact


3 Answers

I put together an example to show off the scoping rules here:

struct Foo {
    a: i32,
}

impl Drop for Foo {
    fn drop(&mut self) {
        println!("Foo: {}", self.a);
    }
}

fn generate_temporary(a: i32) -> Option<Foo> {
    if a != 0 { Some(Foo { a: a }) } else { None }
}

fn main() {
    {
        println!("-- 0");
        if let Some(foo) = generate_temporary(0) {
            println!("Some Foo {}", foo.a);
        } else {
            println!("None");
        }
        println!("-- 1");
    }
    {
        println!("-- 0");
        if let Some(foo) = generate_temporary(1) {
            println!("Some Foo {}", foo.a);
        } else {
            println!("None");
        }
        println!("-- 1");
    }
    {
        println!("-- 0");
        if let Some(Foo { a: 1 }) = generate_temporary(1) {
            println!("Some Foo {}", 1);
        } else {
            println!("None");
        }
        println!("-- 1");
    }
    {
        println!("-- 0");
        if let Some(Foo { a: 2 }) = generate_temporary(1) {
            println!("Some Foo {}", 1);
        } else {
            println!("None");
        }
        println!("-- 1");
    }
}

This prints:

-- 0
None
-- 1
-- 0
Some Foo 1
Foo: 1
-- 1
-- 0
Some Foo 1
Foo: 1
-- 1
-- 0
None
Foo: 1
-- 1

In short, it seems that the expression in the if clause lives through both the if block and the else block.

On the one hand it is not surprising since it is indeed required to live longer than the if block, but on the other hand it does indeed prevent useful patterns.

If you prefer a visual explanation:

if let pattern = foo() {
    if-block
} else {
    else-block
}

desugars into:

{
    let x = foo();
    match x {
    pattern => { if-block }
    _ => { else-block }
    }
}

while you would prefer that it desugars into:

bool bypass = true;
{
    let x = foo();
    match x {
    pattern => { if-block }
    _ => { bypass = false; }
    }
}
if not bypass {
    else-block
}

You are not the first one being tripped by this, so this may be addressed at some point, despite changing the meaning of some code (guards, in particular).

like image 51
Matthieu M. Avatar answered Nov 20 '22 00:11

Matthieu M.


It's annoying, but you can work around this by introducing an inner scope and changing the control flow a bit:

fn f3(&mut self) {
    {
        if let Some(x) = self.f1() {
            // ...
            return;
        }
    }
    self.f2()
}

As pointed out in the comments, this works without the extra braces. This is because an if or if...let expression has an implicit scope, and the borrow lasts for this scope:

fn f3(&mut self) {
    if let Some(x) = self.f1() {
        // ...
        return;
    }

    self.f2()
}

Here's a log of an IRC chat between Sandeep Datta and mbrubeck:

mbrubeck: std:tr::Chars contains a borrowed reference to the string that created it. The full type name is Chars<'a>. So f1(&mut self) -> Option<Chars> without elision is f1(&'a mut self) -> Option<Chars<'a>> which means that self remains borrowed as long as the return value from f1 is in scope.

Sandeep Datta: Can I use 'b for self and 'a for Chars to avoid this problem?

mbrubeck: Not if you are actually returning an iterator over something from self. Though if you can make a function from &self -> Chars (instead of &mut self -> Chars) that would fix the issue.

like image 27
mbrubeck Avatar answered Nov 20 '22 00:11

mbrubeck


As of Rust 2018, available in Rust 1.31, the original code will work as-is. This is because Rust 2018 enables non-lexical lifetimes.

like image 4
Shepmaster Avatar answered Nov 20 '22 01:11

Shepmaster