Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does Rust not perform implicit deref coercion in match patterns?

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?

like image 585
Billy Avatar asked Jul 11 '20 15:07

Billy


People also ask

What is Deref rust?

Trait std::ops::DerefUsed for immutable dereferencing operations, like *v . In addition to being used for explicit dereferencing operations with the (unary) * operator in immutable contexts, Deref is also used implicitly by the compiler in many circumstances.

What is coercion in Rust?

Type coercions are implicit operations that change the type of a value. They happen automatically at specific locations and are highly restricted in what types actually coerce. Any conversions allowed by coercion can also be explicitly performed by the type cast operator, as .

What are smart pointers in Rust?

Smart pointers are abstract data types that act like regular pointers (variables that store memory addresses of values) in programming, coupled with additional features like destructors and overloaded operators. They also include automatic memory management to tackle problems like memory leaks.


1 Answers

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.

like image 93
Aplet123 Avatar answered Nov 06 '22 14:11

Aplet123