Confused about bounds in conjunction with GATs

This is a minified version of a problem I ran into in an actual project. I have a trait with an associated type that is generic over a type parameter.

trait Trait {
    type Value<T>;
}

struct Required;
struct Optional;

impl Trait for Required {
    type Value<T> = T;
}

impl Trait for Optional {
    type Value<T> = Option<T>;
}

This trait can then be used to make structs that are generic over parameter optionality:

struct TraitUser<T: Trait>(T::Value<u64>);

impl<T: Trait> TraitUser<T> {
    fn new(value: T::Value<u64>) -> Self {
        Self(value)
    }
}

Now I would like to implement PartialEq for TraitUser, which requires T::Value<u64> to be PartialEq as well. The auto-derive macro works, although it incorrectly requires that T: PartialEq, in addition to T::Value<u64>: PartialEq. This can be worked around by auto-deriving PartialEq for the ZST or by implementing PartialEq manually.

Since the trait is used in multiple places, I would like to associate the required bounds with the trait itself. One (suboptimal but functional) solution I found to encode this, is to restrict the type parameter as well as the resulting type:

trait Trait {
    type Value<T: PartialEq>: PartialEq;
}

The first bound is an obligation for the user of the trait. The second bound is a obligation for the implementer. Unfortunately, this has the side effect of restricting the use of the GAT to Ts that are PartialEq.

Ideally, I would like to express that Self::Value<T> must be PartialEq for any T that is PartialEq -- but I guess that would require HRTBs over types, which is currently not possible. As an alternative, I would like to add bounds to the trait for a number of specific instances of T. I believe this must be done in a where clause.

When adding the bound as a where clause on the GAT, we get overflows on the impls as well as a compile error on the use of the trait. From the second error, it seems the compiler wants us to prove at the use site that the condition holds, which is not what we want.

trait Trait {
    type Value<T>
    where 
        Self::Value<u64>: PartialEq;
}

error[E0275]: overflow evaluating the requirement <Required as Trait>::Value<u64> == _
--> src/main.rs:16:5
|
16 | type Value<T: PartialEq> = T;
| ^^^^^^^^^^^^^^^^^^^^^^^^

error[E0277]: can't compare <T as Trait>::Value<u64> with <T as Trait>::Value<u64>
24 | struct TraitUser<T: Trait>(T::Value);
| ^^^^^^^^^^^^^ no implementation for <T as Trait>::Value<u64> == <T as Trait>::Value<u64>
|
= help: the trait PartialEq is not implemented for <T as Trait>::Value<u64>
note: required by a bound in Trait::Value
--> src/main.rs:4:27

Similarly, if the bound is added on the trait itself, the compiler expects us to prove that it holds everywhere the trait is used (comment the use-site where clauses to see the compiler errors).

trait Trait
where 
    Self::Value<u64>: PartialEq
{
    type Value<T>;
}

error[E0277]: can't compare <T as Trait>::Value<u64> with <T as Trait>::Value<u64>
--> src/main.rs:25:21
|
25 | struct TraitUser<T: Trait>(T::Value)
| ^^^^^ no implementation for <T as Trait>::Value<u64> == <T as Trait>::Value<u64>
|
= help: the trait PartialEq is not implemented for <T as Trait>::Value<u64>
note: required by a bound in Trait
--> src/main.rs:3:23

This confuses me, and it seems to be in contradiction to the section on supertraits in the Rust Reference, where the where bound on a trait is used to define a supertrait (i.e. to place an obligation on the implementer and provide a guarantee to the user of the trait).

My questions are as follows:

  • Is this the expected compiler behavior, and if so, why?
  • Is there currently a way to add the bound on (specific instantiations of) the GAT to the trait as an obligation to the implementer and a guarantee to the user of the trait?

I think this an instance of the problem that as of now makes GAT type parameters not as useful as one might think: the fact that they’re either underconstrained to the point of uselessness – because impls cannot constrain them further – or alternatively overconstrained to the intersection of all bounds required by some finite set of possible use cases.

1 Like

It's interesting... I recall learning the super trait definition as

trait Foo: Bar {}

instead of what's present in the reference

trait Foo where Self: Bar {}

I wonder if the compiler only works most cleanly when Self is what's being restricted? (not an associated type)

(Just two criticisms, feel free to ignore. I stopped reading at error so not dealing with what you want.)

Naming choice is bad for comprehension; Maybe better (but I'm still not sure)

trait Field {
    type Type<T>;
}

I struggle to understand why to bother with extra complication and not just.
struct TraitUser<T: PartialEq>(T);
Or PartialEq defined for implementation, if wanting not to cover all.

What happens if you try this? (if it fits your use case)

trait Trait {
    type Value<T>: PartialEq;
}

Then it becomes impossible to implement the trait in a useful way: Value<T> must implement PartialEq (for any T) without relying on T implementing PartialEq itself. That means the PartialEq impl on Value<T> must ignore T.

They can be constrained at the use site, but that requires loads of repeated where bounds...

Do you happen to know of a tracking issue for this?

Those are merely two ways to spell the same thing.

Bound that aren't on Self aren't supertrait bounds, and those aren't elaborated elsewhere.[1]

Bounds directly on associated types/GATs are elaborated elsewhere, and I've seen this described in supertrait terms, but clearly it's not the case for a partial bound like this. :person_shrugging:

However, you can do this with a subtrait.

trait SubTrait
where
    // Note that these are supertrait bounds
    Self: Trait<Value<u64>: PartialEq>,
    // You can't write two GAT bounds in one line (yet?)
    Self: Trait<Value<u32>: PartialEq>,
{}

impl<T> SubTrait for T
where
    T: Trait<Value<u64>: PartialEq>,
    T: Trait<Value<u32>: PartialEq>,
{}

  1. This has upsides as well as downsides. ↩︎

2 Likes