rust

Why does my newtype "inherit" Clone but not Display?


Per this question, if you implement Deref<Target = T> on a type U, you can call T's inherent methods (the methods that are defined in impl T { ... }) on expressions of type U, but you cannot call methods on traits defined for T.

pub struct Thing(String);

impl Thing {
    pub fn new(s: impl Into<String>) -> Self {
        Self(s.into())
    }
}

However, when I try to square this with the Rust Reference section on method call resolution, I get confused. It says the steps to find available methods are:

Obtain [a list of candidate receiver types] by repeatedly dereferencing the receiver expression’s type, ... then, for each candidate type T, search for a visible method with a receiver of that type in the following places:

  1. T’s inherent methods (methods implemented directly on T).
  2. Any of the methods provided by a visible trait implemented by T.

Part 2 sounds like it should disagree with the answer of the question I linked.

When I try this out (playground:

use std::ops::Deref;

struct Thing(String);

impl Thing {
    pub fn new(s: String) -> Self {
        Self(s)
    }
}

impl Deref for Thing {
    type Target = String;
    
    fn deref(&self) -> &String {
        &self.0
    }
}

fn main() {
    let thing = Thing::new(" jup ".to_string());
    // I can call trim on a Thing, because of deref-coercion.
    println!("{}", thing.trim());
    // I cannot call fmt, because that is a trait implemented on the inner type.
    // println!("{}", thing);
    // But, I can call clone...?
    println!("{}", thing.clone());
}

I do indeed find that my outer type doesn't act like it implements Display, even though the inner one does, but I do find that it acts like it implements Clone.

What am I not understanding here? The Rust Reference section I quoted seems like it should find String as a candidate type when I call methods on Thing objects, and I believe both Clone and Display are "visible traits implemented by" String. Yet the question I linked seems to suggest neither should be available (although it doesn't say exactly the same thing - it just says that Thing doesn't implement Clone or Display, maybe that's different from being able to call those traits' methods on expressions of type Thing).


Solution

  • When you invoke a method using dot-syntax, deref coercion applies, and so you can invoke methods of the type you deref to. This can even be applied multiple times, which is why you can call trim, which is on str, not String, because Thing derefs to String, and String derefs to str.

    Under the hood, println!() is doing something like Display::fmt(&thing, _) and deref coercion doesn't work out the way you would think it should here. It might also use a function that bounds on Display, and bounds do not trigger deref coercion:

    fn assert_display(_: &impl Display) {}
    
    fn example(thing: &Thing) {
        assert_display(thing); // compile error
    }
    

    This would fail even for e.g. impl Clone; Display is not special here.

    If you bring Display into scope (use std::fmt::Display;) you will find that you can invoke fmt on it!

    fn example(thing: &Thing, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        use std::fmt::Display;
        thing.fmt(f)
    }
    

    Deref coercion generally applies whenever you take a reference to a T and try to bind it to a reference to U, when there is a chain of Deref implementations from T to U. However, this doesn't work in the case where you are directly invoking a trait method using associated function syntax. I believe this is because when you say Display::fmt(&thing, _), the compiler infers that the Display implementation being called is that of the type of thing. Note that <String as Display>::fmt(&thing, _) does work:

    fn example(thing: &Thing, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        <String as std::fmt::Display>::fmt(thing, f)
    }
    

    This is because we told the compiler exactly which Display implementation to use, and so it can auto-deref from &Thing to &String.

    tl;dr: For one out of several possible reasons, deref coercion doesn't work with println!().