Why isn't Deref coercion performed on trait method calls?

Why doesn't this code compile Rust Playground

struct Foo(usize);

impl From<&str> for Foo {
  fn from(value: &str) -> Self {
    Self(value.len())
  }
}

fn main() {
  let a: &str = "asdf";
  let b: &String = &String::from("asdf");

  let a_len = Foo::from(a);
  let b_len = Foo::from(b);  // Error
}

with

help: the trait `From<&String>` is not implemented for `Foo`
      but trait `From<&str>` is implemented for it

If I understand correctly from the reference on type coercions, a function call is a "coercion site" and the transformation "&T&U where T: Deref<Target=U>" is a "coercion type". I assume this is special because trait methods don't count as "coercion sites"? Writing let b_len: Foo = b.into() also does not work, even though (I would assume) that counts as a method call expression, where auto-deref is also performed?

I would love to understand this more.

Trait methods do count as coercion sites. That’s why this works

    let a_len = <Foo as From<&str>>::from(a);
    let b_len = <Foo as From<&str>>::from(b);

Foo::from can’t perform any coercion because it doesn’t know what type it needs to coerce into until after it selects the impl, and it can’t select an impl without knowing the type of the first parameter.

To break the cycle, tell it which impl to use, through a qualified path.

3 Likes

In general, coercions, including deref coercions, happen when the type provided (such as the type of the function argument, b) and the type needed (such as the type of the function parameter) are both known, but do not match. They don’t happen when the type needed is not yet known.

Ah, I see. I guess intuitively I would expect this to work since there is one (and only one) coercion that makes the given type (&String) become a member of the set of candidate types at the call site of from (i.e. {&str, Foo}).

The issue with that in the general case is (don't quote me on it, only as far as my understanding goes) that assume Foo is from a different crate. Then implementing From<&&String> for Foo in a later version would now be semver-breaking because someone downstream could have relied on the From<&str> impl being the only applicable one, and now a error occurs because one would need to explicitly specify.

"Only a single implementation" does guide... trait solving I believe,[1] not coercion per se. But it's not as in-depth as your intuition. If there is only one implementation, your OP succeeds. It knows the target type is &str (and can coerce accordingly once that is known).

But when you add another implementation, it fails. It doesn't get as far as picking a target type. It doesn't probe possible coercions over all potential target types in an attempt to winnow the possibilities.

Thus the "only a single implementation" guidance does indeed cause inference breakage when adding implementations. That is technically considered a SemVer acceptable change, but on a practical level, it does limit std and probably other popular crates. (If the breakage is small enough, sometimes they do it anyway. But things go poorly if they judge wrong.)


  1. ? ↩︎