Why doesn't this example in std.String compile?

Take the second example in this section (that I copy below.)

It does not compile (and it is meant as such).

The snippet implements a trait TraitExample for &str and then uses it as a trait bound for the function generic parameter A.

The snippet is:

    trait TraitExample {}
    impl<'a> TraitExample for &'a str {}

    fn example_func<A: TraitExample>(example_arg: A) {}

    let example_string = String::from("example_string");
    example_func(&example_string);

They say:

In this case Rust would need to make two implicit conversions, which Rust doesn’t have the means to do.

However, if the type passed T implements Deref<Target=U> then &T would dereference to &U. Even if it must do it many times:

    fn example_func(example_arg: &str) {}
    let example_string = String::from("example_string");
    example_func(&&&&&&example_string);

I understand that the compiler can not do everything for us. If there is a simple trait bound, it is not just for &str but a general restriction.

But shouldn't it apply the same autoderef procedure and try to find a type that does implement the trait?

I would say it has to do with something along the lines of what is mentioned here for operators.

TLDR: There are parts of the language that would have issues if deref coercions were applied everywhere.

Deref coercion, not auto-deref.

Deref coercion is based around a known target type, and doesn't do trait solving. If it did, many more use cases would become ambiguous. Probably approximately all use cases, if forward compatibility were to be considered.

But, is the justification they give (cited in OP) correct? @quinedot @firebits.io

Hmmm. Is it technically impossible for all circumstances? Probably not, no.

My take is that the documentation in question is more of a "btw this situation doesn't work" than some formal justification on why it couldn't work, or shouldn't work.

But what does this mean?

two implicit conversions

Isn't going from &String to &str also two implicit conversions?

It's just one deref coercion step.

That said, I don't actually know exactly what the documentation author meant with their phrasing.

My vibe is more along the lines of "some functionality is hindered by generics" (this example, implicit reborrows...), but I'm afraid I don't have a citation or fleshed-out mental model to offer in this case.

Rust only performs deref coercions if the target type is known unambiguously. That isn't the case when it's a generic, so no coercions happen at all in that scenario.

2 Likes

This may not be how one should think about it, but in case you can correct me:

So it does not use the information that, in this case, the generic is actually a string ?

Do you mean the information that only &str implements the trait? No because rustc tries to be robust in case you add more implementations in the future.

Tomorrow you might add

impl<'a> TraitExample for &'a String {}

and that should not silently change the behavior of example_func(&example_string).

I know that there are some exceptions to this principle, but it does apply most of the time.

I meant that these pieces of information are available:

  1. We are passing a &String so T=&String
  2. The signature indicates T:SomeTrait
  3. The specific call of the function uses T=&String which does not directly implement the trait.

So it seems it would make sense to say: maybe the trait is in the Target of Deref? And so it does the dereference.

I don't see ambiguity there, but I don't think this should be this way, just don't quite see whether it couldn't in principle.

...and then you add the implementation described by Alice, and this call silently changes its behavior. Which, in general, is treated as worse than even explicit breakage.

If a function with the same name and self parameter than a trait is implemented inherently, it also overrides the trait function.

Isn't this the same case? And it is allowed.

No, it's not the same. Think about a multi-crate scenario and who breaks who.

// Crate A.
trait MyTrait {
    fn foo(&self);
}

/// Crate B.
struct MyStruct;
impl MyTrait for MyStruct {
    fn foo(&self);
}

If we add an inherent foo method to MyStruct, then that change happened in crate B, so crate B broke itself. Not great, but acceptable.

On the other hand, consider our original scenario:

// Crate A.
trait TraitExample {}
impl<'a> TraitExample for &'a str {}

fn example_func<A: TraitExample>(example_arg: A) {}

// Crate B.
let example_string = String::from("example_string");
example_func(&example_string);

If someone adds an impl TraitExample for &String, then that happens in crate A due to the orphan rule. So it would be crate A breaking a downstream crate B. We want to design Rust such that adding a new implementation of a trait is not a breaking change, but if crate B used deref coercion when calling example_func, then it would be a breaking change. Therefore, we don't use deref coercion.

3 Likes

That makes sense, thanks!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.