Rust beginner here. I am struggling for quite a while with enforcing equality of two generic associate types in a trait bound.
Here is a minimal example of what I try to achieve
trait Trait {
// Trait with a 'normal' type and a GAT
type gat<'a>;
type normal;
}
// Struct A and B define the same types
struct StructA;
impl Trait for StructA {
type gat<'a> = ArrayView2<'a, f64>;
type normal = Array2<f64>;
}
struct StructB;
impl Trait for StructB {
type gat<'a> = ArrayView2<'a, f64>;
type normal = Array2<f64>;
}
struct C<S, T>
where
S: for<'a> Trait<gat<'a> = T::gat<'a>, normal = T::normal>,
T: Trait,
{
a: S,
b: T,
}
fn test() {
C {
a: StructA {},
b: StructB {},
};
}
The normal type does not cause any problems. But despite being StructA::gat equal to StructB::gat I get the following error–but only in the test function:
error[E0271]: type mismatch resolving `for<'a> <StructA as Trait>::gat<'a> == <_ as Trait>::gat<'a>`
--> src/main.rs:51:12
|
51 | a: StructA {},
| ^^^^^^^^^^ type mismatch resolving `for<'a> <StructA as Trait>::gat<'a> == <_ as Trait>::gat<'a>`
|
note: expected this to be `<_ as Trait>::gat<'_>`
--> src/main.rs:30:20
|
30 | type gat<'a> = ArrayView2<'a, f64>;
| ^^^^^^^^^^^^^^^^^^^
= note: expected associated type `<_ as Trait>::gat<'_>`
found struct `ndarray::ArrayBase<ndarray::ViewRepr<&f64>, ndarray::Dim<[usize; 2]>>`
= help: consider constraining the associated type `<_ as Trait>::gat<'_>` to `ndarray::ArrayBase<ndarray::ViewRepr<&f64>, ndarray::Dim<[usize; 2]>>` or calling a method that returns `<_ as Trait>::gat<'_>`
= note: for more information, visit https://doc.rust-lang.org/book/ch19-03-advanced-traits.html
note: required by a bound in `C`
--> src/main.rs:42:22
|
40 | struct C<S, T>
| - required by a bound in this
41 | where
42 | S: for<'a> Trait<gat<'a> = T::gat<'a>, normal = T::normal>,
| ^^^^^^^^^^^^^^^^^^^^ required by this bound in `C`
The gat trait for each struct works as I expect (allowing for using the type as gat<'_> in method parameters). And StructC should be generic. How can I resolve this issue?
Thanks
EDIT:
My temporary solution is to impose no bounds on the struct; Impose Into/From as suggested by @semicoleon for ::new, and impose the original bounds on the relevant trait (I would From/Into also for references).
Rephrasing the bound in terms of conversions works. If that fits your actual use case that might be a better design since it doesn't force equality when it isn't strictly necessary
From the error message it looks like the trait solver is having trouble inferring what the type of T should be in the bound on S. And bizarrely specifying T in test actually does fix the error.
However generally speaking, it's more idiomatic to no put bounds on your structs unless required (e.g. you have a field with the type of an associated type), and put the bounds on implementations or function declarations where they are needed instead.
(Bounds on the struct aren't automatically elaborated elsewhere, so they have to be repeated everywhere anyway if you put them on the struct.)
Then you can always construct the type directly, but it may error depending on the methods called.
While continuing to find a solution before going to sleep, I induced this (obviously erroneous) cyclic dependency by implementing Trait for StructC. However, the compiler/RustAnalyser don't terminate anymore. Is this worth raising a bug (compiler should not engage in an endless loop)?
impl<S, T> Trait for StructC<S, T>
where
for<'a> S: Trait<gat<'a> = Self::gat<'a>, normal = Self::normal>,
for<'a> T: Trait<gat<'a> = Self::gat<'a>, normal = Self::normal>,
{
type gat<'a> = S::gat<'a>;
type normal = S::normal;
fn test(&self) {
println!("hello world")
}
}
PS: Thank you very much for fast and helpful replies. Amazing!
@quinedot , Why is imposing the bounds on the structs considered not idiomatic. Users of the library should be warned during the construction and not when methods of a trait are not available. However, as I provide a new static method with that bounds, I agree.
Just wondering, is the whole pattern un-idiomatic? I use it for a library that is generic in its numeric library; and data is often specified in a subset of elements (here ArrayView) that require a lifetime. The recent stabilization of GAT seem an elegant way of doing so
Strangely, while your recommendation (moving the trait bound in the implementation) generally works, it does not work for a ::new constructor. This fails with the same error message:
impl<S, T> StructC<S, T>
where
S: for<'a> Trait<gat<'a> = T::gat<'a>, normal = T::normal>,
T: Trait,
{
fn whatever(self) {}
fn new(a: S, b: T) -> StructC<S, T> {
StructC { a: a, b: b }
}
}
fn main() {
// Works
let a = StructC {
a: StructA {},
b: StructB {},
};
a.whatever();
// Fails
let a = StructC::new(StructA, StructB);
}
My apologies, the error does indeed persist with that pattern too. I thought I had tested it, but looking back at my playground I see I used a reduced bound (from otherwise playing around with the example).
It's probably not idiomatic in part because it's annoying to program: If you add the bounds to the struct, you have to repeat those bounds everwhere the struct is used, even if that use site does not need the bounds.
However there are other reasons too: By putting bounds on the struct, you require them everywhere the struct is used, which can hinder other code in generic contexts. This example will be a bit contrived, but imagine I'm trying to use your library I have some enum:
And I want to use this in the context of some set of traits or whatever; the Var1 variant never gets constructed unless T: Hash + Eq, and the Var3 never gets constructed unless T: YourTrait.
But if you put the T: YourTrait bound on YourStruct, I have to enforce that bound even if I don't need it! Maybe you didn't implement it for Arc<str>; now I can't have a SomeEnum<Arc<str>> even though it would otherwise work perfectly well with the other two variants.
If there is some U: YourTrait which doesn't implement Hash or Eq, though, that will still work for me -- because HashSet didn't put those constraints on the struct itself, so HashSet<U> is still "well formed".
There is an accepted implied bounds RFC that would elaborate bounds put on the struct when they are used elsewhere. There are some use cases for this I care about (around traits), but in the general form (where it applies to bounds on structs) the RFC quite concerns me:
The bounds will be declared farther away from where they are enforced, so it will be harder to read a function signature (for example) and know the implied constraints unless you've memorized the declaration of every type in the function signature
It becomes a breaking change to remove a bound from your struct
It's less flexible for others as outlined above
It removes the "annoying to program" part which may sway the ecosystem to prefer putting bounds on structs, which will make the ecosystem and programming in Rust worse overall as per the previous bullet points