11

Background

Consider a toy problem in which I have a Node struct that represents nodes of linked lists and that I want to create a function that builds a list with values from 1 to 9. The following code works as expected:

struct Node {
    val: i32,
    next: Option<Box<Node>>,
}

fn build_list() -> Option<Box<Node>> {
    let mut head = None;
    let mut tail = &mut head;
    for n in 1..10 {
        *tail = Some(Box::new(Node {val: n, next: None}));
        if let Some(ref mut x) = tail {
            tail = &mut x.next;
        };
    }
    head
}

But if I modify the match expression in the build_list function to the following, it fails to compile:

fn build_list() -> Option<Box<Node>> {
    let mut head = None;
    let mut tail = &mut head;
    for n in 1..10 {
        *tail = Some(Box::new(Node {val: n, next: None}));
        if let Some(x) = tail.as_mut() {
            tail = &mut x.next;
        };
    }
    head
}

The compilation error:

error[E0506]: cannot assign to `*tail` because it is borrowed
  --> src/main.rs:72:9
   |
72 |         *tail = Some(Box::new(Node {val: n, next: None}));
   |         ^^^^^
   |         |
   |         assignment to borrowed `*tail` occurs here
   |         borrow later used here
73 |         {
74 |             if let Some(x) = tail.as_mut() {
   |                              ---- borrow of `*tail` occurs here

error[E0499]: cannot borrow `*tail` as mutable more than once at a time
  --> src/main.rs:74:30
   |
74 |             if let Some(x) = tail.as_mut() {
   |                              ^^^^ mutable borrow starts here in previous iteration of loop

Question

In this example, what is the difference between

if let Some(ref mut x) = tail

and

if let Some(x) = tail.as_mut()

?

(As a beginner learning Rust) I was expecting those match expressions to be equivalent, but apparently there's some subtle difference that I'm missing.

Update

I cleaned up the code from my original example so that I don't need a placeholder element for the head of the list. The difference (and compilation error) still remains, I just get an additional compilation error for assigning to borrowed *tail.

Update 2

(This is just a curious observation, doesn't help answering the original question) After considering @Emoun's answer, it sounded important (in the first working example) that the compiler should be able to know that tail was changing at every iteration of the loop (such that it could make sure &mut x.next being borrowed each time was different). So I made an experiment to change the code in a way that the compiler wouldn't be able to tell if that was the case by adding a if n % 2 == 0 condition to the tail = &mut x.next; assignment. Sure enough, it resulted in a compilation error similar to the other one:

fn build_list() -> Option<Box<Node>> {
    let mut head = None;
    let mut tail = &mut head;
    for n in 1..10 {
        *tail = Some(Box::new(Node {val: n, next: None}));
        if let Some(ref mut x) = tail {
            if n % 2 == 0 {
                tail = &mut x.next;
            }
        };
    }
    head
}

The new error:

error[E0506]: cannot assign to `*tail` because it is borrowed
  --> src/main.rs:60:9
   |
60 |         *tail = Some(Box::new(Node {val: n, next: None}));
   |         ^^^^^
   |         |
   |         assignment to borrowed `*tail` occurs here
   |         borrow later used here
61 |         if let Some(ref mut x) = tail {
   |                     --------- borrow of `*tail` occurs here

error[E0503]: cannot use `*tail` because it was mutably borrowed
  --> src/main.rs:61:16
   |
61 |         if let Some(ref mut x) = tail {
   |                ^^^^^---------^
   |                |    |
   |                |    borrow of `tail.0` occurs here
   |                use of borrowed `tail.0`
   |                borrow later used here

error[E0499]: cannot borrow `tail.0` as mutable more than once at a time
  --> src/main.rs:61:21
   |
61 |         if let Some(ref mut x) = tail {
   |                     ^^^^^^^^^ mutable borrow starts here in previous iteration of loop
luca_moller
  • 156
  • 7
  • 2
    `if let Some(x) = tail` seems to work, too. Unfortunately I do not have an explanation for any of the cases. – CoronA Jul 13 '20 at 07:24
  • 1
    It seems that lifetime of returned value of `tail.as_mut()` is the same as `tail`, thus simply a compiler limitation. Moving `let tail = &mut head` inside the loop make it work. – Kitsu Jul 13 '20 at 08:46
  • 1
    [This](https://stackoverflow.com/questions/62817320/why-does-the-borrow-checker-disallow-a-second-mutable-borrow-even-if-the-first-o/62819653) looks very similar. – Kitsu Jul 13 '20 at 08:48
  • 2
    Whoever wonders why `Some(x)` can match a type of `&mut Option>` should look for [rust match ergonomics](https://github.com/rust-lang/rfcs/blob/master/text/2005-match-ergonomics.md). – CoronA Jul 13 '20 at 15:14
  • @CoronA , `if let Some(x) = tail` does indeed work, and that was surprising to me as well (learnt something new, thanks!). – luca_moller Jul 13 '20 at 22:18
  • @Kitsu , moving `let tail = &mut head` inside the loop would break the program's logic in this case. I did see [this question you pointed](https://stackoverflow.com/questions/62817320/why-does-the-borrow-checker-disallow-a-second-mutable-borrow-even-if-the-first-o/62819653) before and was wondering if it was a similar scenario as well. I still don't understand exactly what makes calling `tail.as_mut()` different than borrowing through matching `tail` directly (is it really about the compiler not being able to reason about an additional/separate reference/lifetime as you mentioned?). – luca_moller Jul 14 '20 at 01:33

1 Answers1

5

The reason why the second version of your code fails is because rust's methods/function always borrow whole objects and never parts of them.

What this means in your case is that the tail.as_mut() borrows tail mutably and that this borrow will stay in effect as long as tail is being used:

...
    for n in 1..10 {
        *tail = Some(Box::new(Node {val: n, next: None})); // Error in the second iteration,
                                                           // 'tail' was already borrowed
        if let Some(x) = tail.as_mut() { // <-+ Borrow of 'tail' starts in the first iteration
            tail = &mut x.next;          // <-+ 'tail' now borrows itself
        };                               //   |
    }                                    // <-+ Borrow of 'tail' ends here, after the last iteration
...

Since x is a borrow of tail, &mut x.next is also a borrow of tail, which means tail = &mut x.next is tail borrowing itself. Therefore, the initial borrow of tail cannot go out of scope as long as tail is in scope. tail is used in every iteration, so the borrow can only go out of scope after the last iteration of the loop.

Now, why does the first version of build_list work? In short: because tail is never borrowed. if let Some(ref mut x) = tail is a destructuring of tail into its components (in this case the Option::Some and x). This doesn't borrow tail as a whole, it just borrows x. When you then tail = &mut x.next, you now also destructure x into its components (extracting only next), and borrow that using tail. In the next iteration, tail is not borrowed and can therefore happily be reassigned.

Method/function calls are limited in that they don't know which parts of an object you will use later. Therefore, as_mut() has to borrow the whole tail, even though you are only using part of it. This is a limitation with the type system and is one reason why getter/setter methods are weaker/more limiting than calling a struct's members directly: getters/setters will force you to borrow the whole struct, while accessing a member directly will only borrow that member and not the others.

Emoun
  • 2,297
  • 1
  • 13
  • 20
  • This still doesn't explain why it works with `tail = &mut head` (instead of `&mut x.next`) for `as_mut()` case. – Ömer Erden Jul 14 '20 at 10:45
  • 2
    I don't think the logic of the program is the same if you do that. But, it does compile because if you assign all tails to `head`, you never have `tail` borrow itself, they will all be borrowing `head`. – Emoun Jul 14 '20 at 10:56
  • I believe logic is not the focus in this case also I don't think _borrow itself_ is a correct argument, since `tail` is a pointer, `tail`'s pointee will be re-borrowed after calling `tail.as_mut()`, then `tail` will borrow `x.next` as mutably. With all these yes `x` is borrowed mutable for multiple times(not the `tail`). If you use `tail = &mut head` our `x` is going to be `head` and `head` is going to be borrowed multiple times as mutable, so why it allows borrowing head for multiple times? – Ömer Erden Jul 14 '20 at 12:37
  • From my understanding of the Non-Lexical Lifetimes RFC (https://github.com/rust-lang/rfcs/blob/master/text/2094-nll.md): The borrow checker has no notion of "pointer". It doesn't even really care about types. When you write `&mut tail`, it records that a variable name `tail` is being borrowed. Doesn't matter whether that variable is already a borrow of something else. `tail.as_mut()` is syntax sugar for `Option::as_mut(&mut tail)`, so clearly a borrow of `tail`. So, from my understanding, `tail` can definitely borrow itself, and it doesn't matter that it is only a reference. – Emoun Jul 14 '20 at 12:47
  • In the first version, why the compiler doesn't complain that `x.next` was already borrowed from a previous iteration? Is it because in this case it can guarantee that it is a different `x.next` each time? Does borrowing a member instead of the whole thing give a better hint to the compiler that what is being borrowed is actually different each iteration? – luca_moller Jul 14 '20 at 13:56
  • Please check [as_mut](https://doc.rust-lang.org/src/core/option.rs.html#292) implementation, `if let Some(x) = tail.as_mut()` is roughly the same with `if let Some(ref mut x) = &mut *tail`. I've used pointer notion because duty of borrow checker is to prevent dangling pointers, if we cannot explain this behavior in pointer level so this might be a clue that could be borrow checker's problem. – Ömer Erden Jul 14 '20 at 14:00
  • 1
    @luca_moller `x` in each iteration are different variables since you make a new one with `if let Some(ref mut x) = tail`. So there is no borrow connected to `x` that survives from one iteration into the next. And yes, borrowing `x` allows the compiler to see that there is no borrow that survives from one iteration to the next. – Emoun Jul 14 '20 at 14:27
  • @ÖmerErden the implementation of `as_mut` is irrelevant. Borrow checking does not look at the implementation of a method, only the signature is relevant. Assuming references are like pointers is not correct in the context of Rust. You can say they are broadly similar and that references often end up being implemented as pointers, however, there is much more to references than that. The borrow checker does more than prevent dangling pointer: It prevents iterator invalidation and race conditions (I can't think of other things right now). – Emoun Jul 14 '20 at 14:38
  • 1
    Thanks for letting me know that borrow checker has more features :) ... just ignore `as_mut`, `&mut *tail` also breaks the borrow checker rules as same as `as_mut()`. – Ömer Erden Jul 14 '20 at 14:44
  • Thanks for the explanations @EmadJ.Maroun. I've added another update to the question doing some exploration around what happens when the compiler can't tell that `x.next` is different at each iteration. – luca_moller Jul 16 '20 at 00:40