Methods implemented for specialized structs are said duplicates when specializing over exclusive traits

Hello everyone, I'm facing a limitation from the rust compiler I do not find fair, could someone explain me why this cannot be done ?

When adding method to a generic struct specialized over a concrete type, everything works fine as usual:

struct MyStruct<T> {
    content: T,
}
trait MyTrait {
    type U;
}

impl MyStruct<u32> {
    fn method() {}
}
impl MyStruct<u64> {
    fn method() {}
}

no compilation error

What I am trying is doing the same but with a trait instead of a concrete type.

struct MyStruct<T> {
    content: T,
}
trait MyTrait {
    type U;
}

impl<A: MyTrait<U=u32>> MyStruct<A> {
    fn method() {}
}
impl<A: MyTrait<U=u64>> MyStruct<A> {
    fn method() {}
}

As you can see, MyStruct<MyTrait<U=u32>> and MyStruct<MyTrait<U=U64>> cannot be the same type since MyTrait<U=u32> and MyTrait<U=u64> cannot be implemented for the same type

One can easily check that by commenting the second impl and try to call MyStruct<MyTrait<U=u64>>::method . The compiler will say it doesn't exist, just as expected.

Still the compiler is complaining about duplicates

error[E0592]: duplicate definitions with name `method`
  --> src/main.rs:12:5
   |
12 |     fn method() {}
   |     ^^^^^^^^^^^ duplicate definitions for `method`
...
16 |     fn method() {}
   |     ----------- other definition for `method`

For more information about this error, try `rustc --explain E0592`.

This is a known issue with the current trait solver.

There is a somewhat cumbersome workaround and a crate that makes it less painful, but I don’t have personal experience with either.


Edit: I just noticed that your case isn’t exactly the same as you’re trying to bound an inherent impl instead of a trait impl, but I strongly suspect that the underlying issues are the same.

2 Likes

Math is not “fair”. Deal with it.

Because of Rice's theorem, as usual.

That's because question of whether fixed type is equal to some other type or not is decidable.

And now we are talking about non-trivial semantic property. Which are not decidable, in general.

And here's the crux of your problem: it's not possible to decide whether certain MyStruct<T: Foo> and MyStruct<T: Bar> can be ever be implemented for the same struct, for arbitrary Foo and Bar restrictions.

No matter what you do there would be either false positives or false negatives. And because accepting some code that would later become a problem when someone would implement T that is Foo + Bar is bad the decision was made to reject some valid combos instead.

This doesn't prove anything except that compiler couldn't find such implementation. Rice theorem works there, too, thus we couldn't say for sure.

The underlying issue is still the same: Rice's theorem is a bitch. The question is not whether compiler would ever be able to accept arbitrary bounds but about whether certain useful combos should be accepted or not.

Currently limit for rejection is very low: compiler could reason about concrete types but not about any generics, all generics are considered to be implementable even if in reality that's impossible.

That's obviously crazy low bar and it would be desirable to raise it, but you shouldn't expect compiler to know things like Fermat's Last Theorem thus there would always be that unfairness which may be resolved Rust way (reject all “bad” programs and reject some “good” programs, too) or C++ way (accept all “good” programs but then accept some “bad” ones, too).

“Fair” treatment, where all “good” programs are accepted and all “bad” programs are rejected is not possible, unfortunately.

while this is true, it's also completely irrelevant. the OP is not trying to use exclusive trait bounds, they are trying to use exact bounds on an associative type, which is decidable, and should be handled by the new trait solver.

1 Like

It's not really a new-vs-old trait solver concern. More discussion here.

My read the last time I looked into this was that it’s generally desired, but infeasible with the current trait solver. Or at least, getting the new trait solver delivered was a higher priority task for the traits team than retrofitting this feature onto the old one.

And there are still also some questions about whether or not adding this as a feature would be a design trap for the language, so it’s not guaranteed even after the new trait solver is done.

So it’s at least partially a new-vs-old trait solver concern, but that’s not the whole story— That RFC was postponed until after the new trait resolver is ready, so this is definitionally a non-starter until that happens. But even when it does, this pattern won’t just magically start being accepted; there will need to be an RFC process and additional development as well.

3 Likes

Yeah, this is really what I meant.

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.