Generic traits, conflicting implementations

Hello everyone :grinning_face:

I have two scenarios with generics that give me conflicting implementation, where no conflicts are really present. What I'm misunderstanding? Is the compiler simply too much conservative?

  1. Simple trait and generic type. In the first case I implement GenericTrait on SomeStruct<T> where the two T in the impls are disjoint sets and so also the two SomeStruct<T> are disjoint sets (even better, no type implements both TraitA and TraitB). I can't see any conflicting implmenetation.
// traits

trait GenericTrait {}
trait TraitA {}
trait TraitB {}

// impls

struct SomeStruct<T> {
    val: T,
}

impl<T> GenericTrait for SomeStruct<T> where T: TraitA {}
impl<T> GenericTrait for SomeStruct<T> where T: TraitB {}

// error[E0119]: conflicting implementations of trait `GenericTrait` for type `SomeStruct<_>`
// --> src/chapters/test.rs:26:1
// |
// 25 | impl<T> GenericTrait for SomeStruct<T> where T: TraitA {}
// | ------------------------------------------------------ first implementation here
// 26 | impl<T> GenericTrait for SomeStruct<T> where T: TraitB {}
// | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `SomeStruct<_>`
//
// For more information about this error, try `rustc --explain E0119`.
// error: could not compile `rust-handbook` (lib) due to 1 previous error
  1. Generic trait and simple type. The second case uses generic traits. The scenario is different, but it's clear that the two T in the GenericTratit<T> impls are disjoint sets, so I can't see any conflict.

// traits
trait GenericTrait<T> {}
trait TraitA {}
trait TraitB {}

// impls

struct SomeStruct {}

impl<T> GenericTrait<T> for SomeStruct where T: TraitA {}
impl<T> GenericTrait<T> for SomeStruct where T: TraitB {}

// error[E0119]: conflicting implementations of trait `GenericTrait<_>` for type `SomeStruct`
// --> src/chapters/test.rs:11:1
// |
// 10 | impl<T> GenericTrait<T> for SomeStruct where T: TraitA {}
// | ------------------------------------------------------ first implementation here
// 11 | impl<T> GenericTrait<T> for SomeStruct where T: TraitB {}
// | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `SomeStruct`
//
// For more information about this error, try `rustc --explain E0119`.
// error: could not compile `rust-handbook` (lib) due to 1 previous error

Thanks for the help!

1 Like

It's possible to have a type that implements both TraitA and TraitB. The compiler is preventing the conflict that would occur in that case. We would need some way of telling it that the traits are mutually exclusive to make it work.

Can't see why the error appears at this point, honestly. My code is fine, now there are no conflicts in view.

The error should appear when a type that implements both appears in code.

Your example is small enough for perfect knowledge of every possibility, but it can easily become impossible to tell. Blanket implementations of the required traits can easily overlap and making them public allows other crates to implement them in ways you didn't intend.

That could make it a breaking change to write new implementations that use any of these traits. Not to mention that you don't get any warning of it in advance.

That's because the type T may be a custom type implemented in downstream crate.

For the first case:

trait GenericTrait {}
trait TraitA {}
trait TraitB {}

struct SomeStruct<T> {
    val: T,
}

impl<T> GenericTrait for SomeStruct<T> where T: TraitA {}
impl<T> GenericTrait for SomeStruct<T> where T: TraitB {}

// ----- The following is what your user may do. -----

struct Foo;
impl TraitA for Foo {}

// Oh no, how to implement `GenericTrait` for `SomeStruct<Foo>`? :(
impl TraitB for Foo {} 

To prevent the downstream crate from creating that type, you may like to use some Orphan Rules tricks.

trait GenericTrait {}
trait TraitA {}
trait TraitB {}

// impls

struct SomeStruct<T> {
    val: T,
}

impl<T> GenericTrait for SomeStruct<(T,)> where (T,): TraitA {}
impl<T> GenericTrait for SomeStruct<(T,)> where (T,): TraitB {} // fine :D

struct Foo;

// // downstream crate is not allowed to do that
// impl TraitA for (Foo,) {} 

The principle of the second case is the same, the problem is not on whether GenericTrait or SomeStruct have T.


If you don't want to introduce weird type wrappers, but still want TraitA and TraitB to be mutually exclusive, then you can define a trait to let the user to choose which way to implement GenericTrait for SomeStruct<Self>. Here is an example.

@Fancyflame, for the first case:

// ----- The following is what your user may do. -----

struct Foo;
impl TraitA for Foo {}

// Oh no, how to implement GenericTrait for SomeStruct<Foo>? :frowning:
impl TraitB for Foo {}

Can't see why that should be my problem? :thinking: Wouldn't make more sense to consider my crate OK and move the error to the user crate? They are creating the conflicting implementation, not me. They will just find out that implementing both traits on a type is not allowed.


@ogeon

Blanket implementations of the required traits can easily overlap and making them public allows other crates to implement them in ways you didn't intend.

That could make it a breaking change to write new implementations that use any of these traits. Not to mention that you don't get any warning of it in advance.

Mmmh, can you provide an example where this may be problematic?

As far as I know, a blanket implementation from TraitA to TraitB can only be implemented by me (both are local), and if I do it my code doesn't compile (because of the newly generated conflict), but till then my code remains ok (ok = no conflicts).

Well, yes, there would need to be a blanket implementation written by you if they aren't public. My point was more that the case where it's "fine" is relatively slim and limited to when the implementations aren't open ended in any way. So no public traits, no blanket implementations, no other overlap, etc.

I don't think it's a matter of who's responsible for solving the conflict, although Rust tends to place the correctness burden on the "owner" of the type or function. I think it's rather that it hasn't been a priority to add a special check for this compared to cracking the specialization nut, with is a superset of (or at least closely related to) this situation and has been on the back burner for a long time.

But I don't know what the latest news are there.

Well, the simplest answer is "rustc is not smart enough", but that's too general for any question.

For further explaination, Rustc will only judge the confliction by the type you are implementing for. The type is same, trait is same, the confliction exists. You are trying to implement GenericTrait for SomeStruct<T> twice, that would be considered to be confliction, regardless of what traits they were constrained by.

For the exact reason why rustc to do so, I have no idea, maybe because the complexity and hidden trouble. If you have no intention to make the two trait conflicting, but you did it, and no further hints given, your user will encounter a confusing error some time in the future, maybe after some years.

At least in your crate, you are not expressed that "I want TraitA and TraitB to be conflict", rustc cannot guess your intention. But if I remember correctly, the conflictable trait is already a nightly feature.

And an important reason is, you can accomplish conflicting-traits by some tricks. The example I gave above is a more general method, user must pick one trait manually as the way to impl. In practical use, you have more easier methods.

In practical use, if your user observe the conflicting-trait rule (which made by you), you are able to design an API to let user avoiding selecting behavior.

That's how C++ does things.

You picked language that tries to prevent such problems in advance.

This may happen with them using some kind of derive from yet another crate.

No, that's not the answer. Developers of Rust simply wanted to make such errors impossible. They made them impossible. Whether that's good decision or not is up to debate, but that's how the language is designed, ultimately.

Exprience with C++ where each upgrade of compiler or any library may bring sudden problems because of such hidden issues. Rust doesn't prevent them totally, but it tries to make them hard—and thus upgrade easy.

Whether that's decision is good or not is up to debate, but the idea is that implementation of addition trait for the type should not lead to the compile-time error. Note that what Rust is harder than what C++ does, so it's not like Rust developers are lasy. Story is radically different: C++ developers desperately wanted to do what Rust did about 20 years ago, but were unable to achieve that in a backward compatible manner (and making C++11 incompatible with C++03 was decided not to be a good idea).

Naturally Rust, being a new language, adopted that idea, because it had not compatibility concerns, at the time.

3 Likes

What type of issues are you talking about, precisely? What's the problem the users will face when using my crate that has something like :right_arrow_curving_down:?

trait GenericTrait {}
trait TraitA {}
trait TraitB {}

impl<T: TraitA> GenericTrait for T {}
impl<T: TraitB> GenericTrait for T {} 

The only possible trouble I can see happening is if I have this in my previous version of the crate:

// my crate, OLD version
trait GenericTrait {}
trait TraitA {}
trait TraitB {}

impl<T: TraitA> GenericTrait for T {}

// user code
struct Foo;
impl TraitA for Foo {} // ✅ all good --> Foo auto-implements GenericTrait
impl TraitB for Foo {} // ✅ all good

Then I push a new version of the crate + user updates the dependency, so this may happen:

// my crate, NEW version
trait GenericTrait {}
trait TraitA {}
trait TraitB {}

impl<T: TraitA> GenericTrait for T {}
impl<T: TraitB> GenericTrait for T {} // <--- 👈🏻 new one

// user code (same as before) + user tries to use new version
struct Foo;
impl TraitA for Foo {}
impl TraitB for Foo {} // <--- ❌ conflicting implementation 

Even in this case seems fine to cause the user code not to compile, on my side I just need to be aware that this type of changes are major version bumps.

You upgrade library (doesn't matter whether it's standard library or some third-party library) and suddenly code that was perfectly fine explodes with weird error messages that you need to track down and fix.

Rust tries to do as much prevention of possible by doing declaration not during instantiation time but by doing that during declaration type. Orphan rule follows the same idea.

That's precisely where where developers of C++ and Rust disagree.

That's not enough: you need to also offer some kind of resolution of the issue. If you had library that defined two traits and now that's forbidden… what exactly downstream should do in that case?

There are many possible solutions and for now Rust picked the simplest one: not to allow that issue to ever be a problem.

Whether that decision is good or bad is debatable. In my experience it's Ok for libraries but is insanely frustrating for apps and sealed traits…

The philosophy behind this check is indeed to prevent problems with the kind of situation you demonstrate. It could theoretically be permitted if we can declare that the traits are intended to be mutually exclusive, so the compiler can prove the correctness and prevent cases where they would overlap.

This is similar to how you are required to declare all the traits you need to use, rather than hoping your users will manage to hold it right. It's a reaction to experiences with C++ and also Ruby, as I remember it, where it's easy to overlook something and get a delayed and obscure problem downstream. It makes you declare the requirements you need upfront for the code to work.

It can also be much more indirect. A case I had in mind with a previous response is a pair of seemingly innocent blanket implementations:

use std::io::{Read, Write};

trait GenericTrait {}
trait TraitA {}
trait TraitB {}

impl<T: TraitA> GenericTrait for T {}
impl<T: TraitB> GenericTrait for T {} 

// May or may not overlap depending on the dependency graph:
impl<T: Read + Write> TraitA for T {}
impl<T: Iterator> TraitB for T {}

These may work for a while and suddenly stop working for a seemingly unrelated reason when a dependency of a dependency changes. It doesn't even have to originate from your or the user's code. The exact traits don't matter, it's the fact that it can have distant consequences that's an issue.

Adding a trait implementation is intentionally designed to be a non-breaking change. Mutually exclusive traits as a (distinct) feature have been desired but blocked on a new trait solver and design considerations since approximately forever ago. The situation isn't going to change any time soon, I reckon, so most of this thread to date is a discussion about language design.

Well, maybe you came to talk about the language design. But if you're looking for some practical workaround or alternative instead, can you say more about the actual traits involved? Which are local, which are public, are they meant to be implemented by downstream or just the owning trait, any associated types, etc.

2 Likes

@khimru

Well you know that you have updated your dependency, so tracking down the issue shouldn't be that difficult (I suppose). Options: 1) you (user) revert to prev version 2) update your code. I don't like the idea of upstream crates to be limited in this way (the users should adapt, to the authors).

But I also see that this approach has some clear advantages

Options: 1) you (user) revert to prev version 2) update your code (up to the user). I'm not saying this is the best thing to have in the lang, just a possible approach.


@ogeon

Mmmmh this can't be implemented by an external crate cause T is not local and TraitA/B is also not local (orphan rule).


@quinedot

Yeah, mostly trying to understand what's the logic behind this.

Really? This may explain the rationale behind this conflicting impl error.

Those would be in the same crate as TraitA and TraitB in this example.

While I certainly wouldn't be on the edge of my seat expecting negative impls to arrive soon, I do want to point out that one of the 2025H2 goals is to continue work on the stabilization of the next-generation trait solver. We're working towards a future where this might be possible.

2 Likes

:+1:

Mostly, yes. But I'm glad you clarified, because my brief "explanation" was too dismissive for talking about language design.[1]

The exceptions are fundamental traits,[2] and blanket implementations. You are correct that an addition like this:

// Let's say you had released the crate but it didn't have the first
// blanket implementation.  And then you release a new version with:

impl<T: TraitB> GenericTrait for T {} // <--- 👈🏻 new one

is a major breaking change in Rust today, as downstream might have done:

impl TraitB for MyTy {}
impl GenericTrait for MyTy {}

At which point I guess the question returns to, is there a design reason[3] why can't you have both blanket implementations from the beginning?

For one, if either TraitA or TraitB were foreign traits, it may take away the ability to add implementations from others. So the locality of the traits still matters. For example:

impl<T: Clone> GenericTrait for T {}

// This will give you an error that upstream may add the overlapping `Clone`
// implementation.  Any generic implementation where the implementor may be
// upstream has the same consideration.
impl GenericTrait for Box<dyn Send> {}

But let's say all three traits are local. Then you've effectively created a form of mutually exclusive traits: Downstream can implement either TraitA or TraitB for their types, but not both. Assuming the lang design take is still "unlocking mutually exclusive traits needs an RFC'd design", that would be a design reason to disallow this case.

Perhaps if we had sealed traits in coherence and TraitA and TraitB were sealed, so that all the negative reasoning were local, the two blanket implementations could be accepted without unlocking mutually exclusive traits in a broader sense.

There's potentially a lot more to say about mutually exclusive traits, how orphan rules and overlap checks work with local negative reasoning, and so on. But I'll stop for now lest I accidentally write a novel.

I am excited about that! (But am not holding my breath for anything that's not already baked[4] to land any time soon afterwards.)


  1. I was really trying to steer things in a practical direction, but guess that isn't the point of the topic. ↩︎

  2. that is only Sized and the Fn* traits; other fundamental traits cannot be created on stable ↩︎

  3. as opposed to technical reasons, which I believe do also exist ↩︎

  4. TAIT is baked I hope :slight_smile: ↩︎

1 Like

First of all, I'm really impressed by your knowledge of RFCs and details of the language :grinning_face: This RFCs are really cryptic to read honestly (I'm no Rust expert yet).

I'm guessing the important bits are:

Restricting the impls that can be written so crates can add implementations for traits/types they do own without worrying about breaking downstream crates.

and especially

Finally, RFC #1105 states that implementing any non-fundamental trait for an existing type is not a breaking change. This directly condradicts RFC #1023, which is entirely based around “blanket impls” being breaking changes. Regardless of whether the changes proposed to the orphan rules in this proposal are accepted, a blanket impl being a breaking change must be true today. Given that the compiler currently accepts impl From<Foo> for Vec<Foo>, adding impl<T> From<T> for Vec<T> must be considered a major breaking change.

As such, RFC #1105 is amended to remove the statement that implementing a non-fundamental trait is a minor breaking change, and states that adding any blanket impl for an existing trait is a major breaking change, using the definition of blanket impl given above.

Please correct me if I'm wrong :slight_smile:

But also: even if it is a breaking change, why am I not allowed to do it? Breaking changes do happen normally; authors just need to be aware that adding a new blanket impl must be a major version bump.


This one is a bit hard to understand. Is the code in the snippet something I would try to implement (incorrectly) in my crate?

@quinedot ^^^ any thoughts on that? Especially this part: