Inconsistency: deref coercion doesn't work

struct S;
trait T {
    fn method(&self) -> &'static str {
        "T"
    }
}
impl S {
    fn method(&self) -> &'static str {
        "S"
    }
}
impl T for S {}

fn main() {
    let b = Box::new(S);
    assert_eq!(b.method(), "S");
    assert_eq!(S::method(&b), "S");

    // Error: the trait `T` is not implemented for `std::boxed::Box<S>`
    // but deref coercion should work here.
    assert_eq!(T::method(&b), "T");
}

Coercions only work if there is no type inference involved, when you say T::method(&b) you are really saying <_ as T>::method(&b), so it won't apply any coercions (not just deref coercions). This is consistent with the rest of the language.

1 Like

What is the particular reason why coercions only work if there is no type inference involved?

Because coercions play badly with type inference, and can lead to some very confusing code. This was an explicit decision to prevent some very weird bugs.

For example, if I added

impl<U: T + ?Sized> T for Box<U> {
    fn method(&self) -> &'static str {
        "Box<_>"
    }
}

Then the behavior of your program would suddenly change, all because of the interaction between coercions and inference. Changes like these lead to code that is harder to maintain.

I think this is how coercions work, weird or convenient. There are some restrictions like "only traits defined in the current crate can be implemented" or "can't impl for type defined outside of crate". It seems there is not a big chance to accidentally cause some confusing problems.

Let's say an upstream crate adds a new implementation (which is entirely possible), this could change the semantics your code just by updating dependencies. (now there are other ways this could happen, but adding a new implementation seems like the sort of thing that won't affect old code, so this is particularly insidious)

Thank you for your wisdom. After impl T for dyn Deref<Target = S> {}, can't still compile. Why is this?

trait T {
    fn method(&self) {}
}
struct S;

impl T for dyn std::ops::Deref<Target = S> {}

fn main() {
    let b = Box::new(S);

    // Error: the trait `T` is not implemented for `std::boxed::Box<S>`
    T::method(&b);
}
trait T {
    fn method(&self) {}
}
struct S;

impl S {
    fn method(&self) {}
}

impl T for S {}

fn main() {
    let b = Box::new(S);

    // auto deref is safe, because `impl &S` is invalid.
    S::method(&b);

    // Error: unsafe to auto deref,
    // because `impl T for &S` or `impl T for Box<S>` is possible.
    T::method(&b);
}

There is a distinction between two steps here: coercions and function lookup. Coercions only apply when the input and output types are known, after which function to call has been determined. Function lookup is a separate process that during method calls looks through possible deref impls and unsized coercions to determine which function is called.

For a normal function call there is no lookup happening, it will look for exactly which function you attempted to call (which is why this was known as "Unambiguous Function Call Syntax"). In this case it sees you attempting to call <S as _>::method(&Box<S>), which is not a function that currently exists, but by adding impl T for Box<S> {} it will. (UFCS isn't actually unambiguous, as far as I know there's no way to force the method to be an inherent method).

Thank you for your informative explanation. If the input and out types are known, can coercions apply to pattern matching? It can't for now.

struct S {
    a: i32,
    b: i32,
}
fn matching(S { a, b }: &S) {}

fn main() {
    let b = Box::new(S { a: 0, b: 0 });
    matching(&b); // this is cool.

    // Error: expected struct `std::boxed::Box`, found struct `S`
    let S { a, b } = &b;
}

That works. Try let S { a, b } = *b; instead.

If Rust has to pick bwtween an inherent method and a trait method, it will always pick the inherent method.

Yes, but you can't write something that will either call an inherent method or fail to compile. For example if you have an inherent method being called via UFCS masking a trait method, when you delete that inherent method it will just fall back to calling the trait method. Whereas you can use UFCS to refer to a method defined by a specific trait, and if that trait is no longer implemented for the type it will fail to compile instead of finding a similarly named method on a different trait/inherent impl.

1 Like

You kind of can ... but it is not pretty :sweat_smile:

6 Likes