After reading the section in the Rust book on Smart Pointers and Interior mutability, I tried, as a personal exercise, to write a function that would traverse a linked list of smart pointers and return the "last" element in the list:
#[derive(Debug, PartialEq)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn get_last(list: &List) -> &List {
match list {
Nil | Cons(_, Nil) => list,
Cons(_, next_list) => get_last(next_list),
}
}
This code results in the following error:
| Nil | Cons(_, Nil) => list,
| ^^^ expected struct `std::rc::Rc`, found enum `List
I was able to get it to work by using a "match guard" and explicitly dereferncing on the Cons(_, x)
pattern:
fn get_last(list: &List) -> &List {
match list {
Nil => list,
Cons(_, next_list) if **next_list == Nil => list,
Cons(_, next_list) => get_last(next_list),
}
}
Given what I've learned about implicit dereferencing and the Deref
trait implementation for Rc
, I would have expected my first attempt to work. Why must I explicitly dereference in this example?
First, we need to understand what deref coercion is. If T
derefs to U
and x
is a value of type T
, then:
*x
is *Deref::deref(&x)
&T
can be coerced to &U
x.method()
will check type U
during method resolution.How method resolution works is when you call a method on a type, it first checks for a method by adding nothing to the type, then adding &
, then adding &mut
, then derefing. So, when figuring out which method to call for x.method()
, it'll first check for a method that takes T
, then &T
, then &mut T
, then U
, then &U
, then &mut U
(read more here). This does not apply to operators. Therefore, ==
will not coerce the differing types, which is why you must explicitly dereference.
But what if we did use a method, like .eq
in the PartialEq
trait? Things get interesting. The following code fails:
fn get_last(list: &List) -> &List {
match list {
Nil => list,
Cons(_, next_list) if next_list.eq(Nil) => list,
Cons(_, next_list) => get_last(next_list),
}
}
but the following succeeds:
fn get_last(list: &List) -> &List {
match list {
Nil => list,
// notice how it's Nil.eq and not next_list.eq
Cons(_, next_list) if Nil.eq(next_list) => list,
Cons(_, next_list) => get_last(next_list),
}
}
Why is this? Let's look at the first example:
next_list
is of type &Rc<List>
, so it starts searching for a .eq
method. It immediately finds one defined in the PartialEq
implementation for Rc
with signature fn eq(&self, other: &Rc<List>)
. However, other
is of type List
in this case, which cannot be coerced to &Rc<List>
.
Then why does the second work?
Nil
is of type List
, so it starts searching for a .eq
method. It can't find any for List
, so it tries &List
next, where it finds the derived PartialEq
implmentation with signature fn eq(&self, other: &List)
. In this case, other is of type &Rc<List>
, which can be coerced to &List
because of its Deref
implementation. This means that everything typechecks properly and the code works.
As to why your first attempt didn't work, it appears to just not be a feature in rust, and there's a proposal to add it dating back to 2017.