Is the following compile error an issue that may get resolved with Rust 2024?
struct A;
struct B;
trait Unique {
type Assoc;
}
trait Outer {}
impl<U: Unique<Assoc = A>> Outer for U {}
impl<U: Unique<Assoc = B>> Outer for U {}
Is the following compile error an issue that may get resolved with Rust 2024?
struct A;
struct B;
trait Unique {
type Assoc;
}
trait Outer {}
impl<U: Unique<Assoc = A>> Outer for U {}
impl<U: Unique<Assoc = B>> Outer for U {}
Hi there, if by “Rust 2024” you are referring the the new edition, then the answer is no. Editions are a mechanism that’s separate from improvements to the trait system anyway. They are mostly about mitigating (otherwise-)breaking syntax changes.
Improving on certain limitations of the trait system is
If you’re curious about general issues that rustc development focuses on, we have a (still relatively new) system that could be of interest: feel free to look into the current status of the “project goals”
Regarding the concrete issues you brought up:
impl<U, const N: usize> Outer for U
where
U: Unique<Assoc = One<N>>,
{}
impl<U, const M: usize, const N: usize> Outer for U
where
U: Unique<Assoc = Two<M, N>>,
{}
this issue that associated types don’t help against overlap-checks is a known issue for a long time, and unrelated to const
generics specifically.
It seems not easy to fix; I’m not sure off the top of my head whether the problem is more with the specific implementation/framework of the trait solver, or more fundamental design problems.
For the time being, you can work around it with some helper trait that would be defined for One<N>
and Two<M, N>
so you can unify the Outer
impl to one single impl
that delegate to the helper trait.
Though looking at your code again, your comments seem to indicate you were instead pointing towards this section?
impl<const N: usize> Unique for NotBoth<N> {
type Assoc = One<N>;
}
// This will conflict
impl<const N: usize> Unique for NotBoth<N> {
type Assoc = Two<N, N>;
}
(I’ve removed the /* */
commenting out the second impl)
And similarly, your other playground has this code…
trait WithConst<const N: usize> {}
trait NamedConst {}
impl<S, const N: usize> NamedConst for S
where
S: WithConst<N>,
{}
those are both compile errors by design, so I’m not quite sure what you’re trying to achieve here in the first place.
That mod guaranteed_unique
was just to prove that Unique
implementations were indeed unique. That confirms that the conflict (in the top level) is indeed impossible - no one type can implement both Unique<Assoc = One<N>>
AND Unique<Assoc = Two<M, N>>
.
And yeah, that second one isn't possible, but was peripherally related. Edited to simplify, I guess it wasn't providing any extra context
Oh, okay, then the point of the first example was about the issue like in the GitHub issue I’ve linked.
To outline the helper-trait workaround here… let’s first also add some method, and set up a way to check the original implementation bodies of any methods would still work (by re-stating them on some other generic functions with the desired bounds). [This is only for demonstrating that the workaround works even in non-stub-example-cases, not part of actually executing it.]
struct One<const N: usize>;
struct Two<const M: usize, const N: usize>;
trait Unique {
type Assoc;
}
trait Outer {
fn some_method(&self) {}
}
impl<U, const N: usize> Outer for U
where
U: Unique<Assoc = One<N>>,
{
fn some_method(&self) {
test_implementation_still_works::_1(self)
}
}
impl<U, const M: usize, const N: usize> Outer for U
where
U: Unique<Assoc = Two<M, N>>,
{
fn some_method(&self) {
test_implementation_still_works::_2(self)
}
}
mod test_implementation_still_works {
…
}
Then we can define the helper trait. The helper trait can use Self
in the role of what used to be the associated type that we wanted to differentiate our behavior on:
trait OuterForUniqueImplHelper<U> {
fn impl_of_some_method(this: &U);
}
Now, we can move the original method bodies into this helper trait.
impl<U, const N: usize> OuterForUniqueImplHelper<U> for One<N>
where
U: Unique<Assoc = One<N>>,
{
fn impl_of_some_method(this: &U) {
test_implementation_still_works::_1(this)
}
}
impl<U, const M: usize, const N: usize> OuterForUniqueImplHelper<U> for Two<M, N>
where
U: Unique<Assoc = Two<M, N>>,
{
fn impl_of_some_method(this: &U) {
test_implementation_still_works::_2(this)
}
}
Finally, write a unified impl for the original 2 impls in terms of this helper:
impl<U> Outer for U
where
U: Unique<Assoc: OuterForUniqueImplHelper<U>>,
{
fn some_method(&self) {
U::Assoc::impl_of_some_method(self)
}
}
If you want to hide this implementation-detail, you can consider things like
OuterForUniqueImplHelper
traitcfg(doc)
so that the generated rustdoc shows the original impls instead [I've never tested this, so there’s a slightly larger than zero chance that rustdoc won't like those impls either; and also users of your crate might still hear about that OuterForUniqueImplHelper
in error messages anyway]That's a pattern I could use, though I'm working up a type system more than trying to compose functions. Ultimately, the issue (including the const generics question) came up because this is the other viable way that I could implement it to avoid conflict, but it introduces an unconstrained type parameter [playground]
trait AsOne<const N: usize> {}
trait AsTwo<const M: usize, const N: usize> {}
impl<T: Target<Assoc: AsOne<N>>, const N: usize> Outer for T {}
impl<T: Target<Assoc: AsTwo<M, N>>, const M: usize, const N: usize> Outer for T {}