Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the difference between matching a mutable Option reference in "if let Some(ref mut x) = option" and in "if let Some(x) = option.as_mut()"?

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
like image 909
luca_moller Avatar asked Jul 13 '20 04:07

luca_moller


1 Answers

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.

like image 173
Emoun Avatar answered Nov 22 '22 06:11

Emoun