Search code examples
rustreferencetraitsdereferencecoercion

Why do I need to implement `From` for both a value and a reference? Shouldn't methods be automatically dereferenced or borrowed?


The behaviour of a Rust method call is described in The Rust Reference. It states that "when looking up a method call, the receiver may be automatically dereferenced or borrowed in order to call a method." The behaviour is explained in more detail further into the chapter.

In the example, you can see that into_char moves Suit, while to_char borrows Suit.

  • into_char, it is called by a value and a reference, and the reference is automatically dereferenced.

  • to_char, it is also called by a value and a reference, and the value is automatically borrowed.

From is implemented for both a value and a reference of Suit. Therefore, when called by a value Into<Suit>::into() is called, and when called by a reference Into<&Suit>::into() is called.

However, shouldn't I only need to implement one of these traits? When I comment out either implementation the Rust compiler does not compile...

The process appears to be as follows... First generate a list of "candidate receiver types". This is obtained by "repeatedly dereferencing", and finally "attempting an unsized coercion at the end". In addition, "for each candidate T, add &T and &mut T to the list immediately after T." Then, for each of these "candidate receiver types" look for "methods implemented directly on T" and "methods provided by a visible trait implemented by T".

Consider that just From<Suit> is implemented for char. Then Into<Suit> should be implemented for char.

  • When let c: char = value.into(); is called, the "candidate receiver types" should contain at least Suit &Suit and &mut Suit. Then, Into<T>::into() is easily resolved for the first item in the list. Hence, Into<Suit>::into() is called.

  • But, when let f: char = reference.into(); is called, the "candidate receiver types" should also contain at least &Suit, &&Suit, &mut &Suit, *&Suit = Suit, &Suit (again) and &mut Suit. Then Into<T>::into() cannot find an implemented for &Suit, &&Suit and &mut &Suit, but does then find an implementation for *&Suit = Suit. Hence, Into<Suit>::into() is called.

Is my logic correct? Why doesn't this work if I comment-out one of the From implementations?

Rust Playground

#[derive(Clone, Copy)]
pub enum Suit {
    Club,
    Diamond,
    Heart,
    Spade,
}

pub use Suit::*;

impl Suit {
    #[inline(never)]
    pub fn into_char(self) -> char {
        match self {
            Club => 'C',
            Diamond => 'D',
            Heart => 'H',
            Spade => 'S',
        }
    }
    
    #[inline(never)]
    pub fn to_char(&self) -> char {
        match self {
            Club => 'C',
            Diamond => 'D',
            Heart => 'H',
            Spade => 'S',
        }
    }
}

impl std::convert::From<Suit> for char {
    fn from(suit: Suit) -> Self {
        suit.into_char()
    }
}

impl std::convert::From<&Suit> for char {
    fn from(suit: &Suit) -> Self {
        suit.to_char()
    }
}

fn main() {
    let value = Club;
    let reference = &value;
    
    let a: char = value.into_char();
    let b: char = value.to_char();
    let c: char = value.into();
    println!("{}, {}, {}", a, b, c);
    
    let d: char = reference.into_char();
    let e: char = reference.to_char();
    let f: char = reference.into();
    println!("{}, {}, {}", d, e, f);
}

EDIT: As discussed below, I created a better reproduction of the problem, which does help narrow down what causes this behaviour.

EDIT: I have created a GitHub issue for the Rust compiler.


Solution

  • This happens because when matching traits the compiler does not take later inference types into account. So, while we know that into() is expected to produce B, the compiler doesn't know that yet. So all it sees is that we want an implementation of Into Suit as Into<_>, for some type _. Because it doesn't know what type _ is, this matches impl<U, T: From<U>> Into<T> for U, seen by the compiler as Into<_> for _, as it is unable to verify the where T: From<U> because _ can be any type. So the compiler picks this implementation (which has higher priority because it does not use autoref) and does not search further for impl Into<_> for &Suit.

    Later, when the compiler comes back to this decision, armed with the knowledge that we expect a char, it realizes that this impl no longer match - but it doesn't go back to search impls for autoref types; it is done with that. So we end up with no matching impl.