Suggestions and questions after reading Rc chapter


#1

After each chapter of the book, I like to experiment further to cement my understanding. After reading about Rc, the concept makes total sense, but I had some renewed difficulties with ref and & in destructuring. I have a decent understanding of how those work in general, but still find myself guessing and checking with compiler hints sometimes.

That’s not necessarily a bad thing. In fact, I wanted to test how well the compiler guides you in the right direction. For reference, here’s the final, working code:

use std::rc::Rc;

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use List::{Cons, Nil};

fn main() {
    let a = Rc::new(Cons(4, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a));
    let c = Cons(5, Rc::clone(&a));

    for mut list in [b, c].iter() {
        while let &Cons(num, ref next_list) = list {
            println!("{}", num);
            list = next_list;
        }
        println!("");
    }
}

Again, I wanted to see how well the compiler guides one in the proper direction, so I removed all the destructuring and binding syntax:

    for mut list in [b, c].iter() {
        while let Cons(num, next_list) = list {
            println!("{}", num);
            list = next_list;
        }
        println!("");
    }

Which produces the following compiler error and hint:

error: non-reference pattern used to match a reference (see issue #42640)
  --> src/main.rs:16:19
   |
16 |         while let Cons(num, next_list) = list {
   |                   ^^^^^^^^^^^^^^^^^^^^ help: consider using a reference: `&Cons(num, next_list)`

So, I make that change:

        while let &Cons(num, next_list) = list {
            println!("{}", num);
            list = next_list;
        }

And get new errors:

error[E0308]: mismatched types
  --> src/main.rs:18:20
   |
18 |             list = next_list;
   |                    ^^^^^^^^^
   |                    |
   |                    expected &List, found struct `std::rc::Rc`
   |                    help: consider borrowing here: `&next_list`
   |
   = note: expected type `&List`
              found type `std::rc::Rc<List>`

Alright, simple enough:

        while let &Cons(num, next_list) = list {
            println!("{}", num);
            list = &next_list;
        }

But again, errors:

error[E0597]: `next_list` does not live long enough
  --> src/main.rs:18:21
   |
18 |             list = &next_list;
   |                     ^^^^^^^^^ borrowed value does not live long enough
19 |         }
   |         - `next_list` dropped here while still borrowed
20 |         println!("");
21 |     }
   |     - borrowed value needs to live until here

error[E0507]: cannot move out of borrowed content
  --> src/main.rs:16:19
   |
16 |         while let &Cons(num, next_list) = list {
   |                   ^^^^^^^^^^^---------^
   |                   |          |
   |                   |          hint: to prevent move, use `ref next_list` or `ref mut next_list`
   |                   cannot move out of borrowed content

Add the ref and we’ve almost got the working code from the beginning. So overall the compiler is directing us mostly the right direction without having to explore too big a combinatorial space. But here’s the version the compiler hints leave us with:

        while let &Cons(num, ref next_list) = list {
            println!("{}", num);
            list = &next_list;
        }

Which can be simplified to:

        while let &Cons(num, ref next_list) = list {
            println!("{}", num);
            list = next_list;
        }

So the second recommended transformation was superfluous, even if it did help us get closer to the real issue. Is there any way the compiler could test compile its own advised transformations and identify the final path here or something?

Also, why the need for ref next_list at all? If one checks the types here, next_list is an &std::rc::Rc<List>, which when bound to list is automatically dereferenced to an &List. If it’s possible to do deref coercion in this manner, and an Rc is already a reference, why the need to add ref to the binding?

This is just a bit confusing and took me a while to grapple with. Perhaps there’s no easy, automatic solution, but then again maybe this could be easier.


#2

(Only a partial answer.)

Cons(i32, Rc<List>) Can immediately see from this I’m looking elsewhere to check the type and coming with something different.
Instead of using the term “destructuring” it is easier in this case to use “pattern matching”. list hasn’t been destroyed, you can read from it again inside the while loop. Instead you gain access to its 2nd field by adding ref; the first field is Copy so without ref a copy is made into num.


#3

Good news! The compiler is being improved in this area. If you try your code with all the destructuring and binding syntax removed on nightly with the match_default_binding feature enabled, it compiles :slight_smile:


#4

Oh wow, neat.

This language has got such a unique focus and the community is great. Thank you!

PS I found the RFC based on your link. Future readers may be curious. https://github.com/rust-lang/rfcs/blob/master/text/2005-match-ergonomics.md