8
votes

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?

1

1 Answers

10
votes

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.