HRTB and GAT lifetimes in trait bounds

Hello,

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

Playground

struct C<S, T>
where
    S: Trait<normal = T::normal>,
    for<'a> <S as Trait>::gat<'a>: Into<T::gat<'a>>,
    T: Trait,
{
    a: S,
    b: T,
}

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.

Playground

fn test() {
    C::<_, StructB> {
        a: StructA {},
        b: StructB {},
    };
}

Moving the bound off of the struct itself and into an impl block also seems to resolve the issue

Playground

struct C<S, T> {
    a: S,
    b: T,
}

impl<S, T> C<S, T>
where
    S: for<'a> Trait<gat<'a> = T::gat<'a>, normal = T::normal>,
    T: Trait,
{
    fn whatever(self) {}
}

fn test() {
    C {
        a: StructA {},
        b: StructB {},
    }
    .whatever();
}
3 Likes

It's worth opening a new issue. The error msg here is like that in Failed to unify types when using GATs and HRTBs · Issue #93341 · rust-lang/rust · GitHub which was solved.

1 Like

The compiler should handle this case, I agree.

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.

3 Likes

Interestingly, the code passes if we reverse the field order: Rust Playground

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 {
        b: StructB {},
        a: StructA {},
    };
}

I learnt the solution from this unanswered issue: Function type with HRTB and associated type argument unifies inconsistently · Issue #101794 · rust-lang/rust · GitHub

1 Like

Heh, I tried reversing the bounds (there is an issue about that mattering), but not the fields.

I tried that too.

After several trials, the secret seems to be the field order of instantiation instead of the one of declaration.

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

@vague , I created this issue

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:

enum SomeEnum<T> {
    Var1(HashSet<T>),
    Var2(Vec<T>),
    Var3(YourStruct<T>),
}

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".

Some related PRs:


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

9 Likes

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.