Carry different implementations for various flavors of GAT

I've got a trait with generic associated types parameterized by elements contained. I'd like to keep the trait neutral and add supertraits naming different functionality for different elements (float vs. int math):

/// Names a generic associated type "container" of general elements `E`
trait NameType: Sized + TypeFloatOps<Self> + TypeIntOps<Self> {
    type WithElements<E: Element>;
}

But, the only way to name a different (yet still open) associated generic (on a non-generic trait) is to name an entirely new generic associated type. So, I name some typed-op supertraits which must be implemented around Self as a way to add a where clause about specific "flavors" of GAT for which <Self as NameType>::WithElements must implement some required behavior.

/// Trait viewing NameType::WithElements<E> with specified `F: Float` elements
trait TypeFloatOps<T: NameType> {
    type NameOps<F: Float>
    where
        T::WithElements<F>: FloatOps,
    ;
}

This works to only allow implementation for types which are implemented for the required ops, as compilation will fail for types where the "flavored" associated type bounds aren't met. So far so good.

BUT! The bounds on the secondary generic associated types don't actually propagate up to the top-level trait, as the trait doesn't guarantee the capability that it is technically guaranteed to be implemented:

// T: NameType should imply TypeFloatOps<Self> which should guarantee float ops
fn float_op<F: Float, T: NameType>(arg: T::WithElements<F>) { arg.float() }

I was worried that my attempt at dependency injection of Self into the supertraits was the issue so I tried flattening the trait but that didn't help. It seems that where clauses on generic associated types can only prevent implementation by un-suitable types, not actually carry any implied trait-bound implementation forward. Is there any way to make this work?

[playground - original]
[playground - flat trait]

Both of your playground link can be compiled with an explicit bound:

fn float_op<F: Float, T: NameType>(arg: T::WithElements<F>)
+where
+    T::WithElements<F>: FloatOps,

The terminology here is implied bounds which is in progress since 2017 when GAT was not a thing in Rust.

We currently support implied bounds for lifetime bounds, super traits and projections. We propose to extend this to all where clauses on traits and types
src: the link above

1 Like

I should probably have added that my where bounds have about a dozen clauses each that I only want to have to write out once. The answer is probably to just accept the fact that I'll need to use trait aliases to hide the repetition. This came out of an attempt to escape trait aliases and nightly.

There are sometimes tricks you can use with non-generic associated types to do things like:

trait Plain {
    type Assoc;
}

trait Decorated: Plain<Assoc = <Self as Decorated>::DecAssoc> {
    type DecAssoc: SomeBound;
}

// And you don't even need to burden implementors
impl<T: Plain> Decorated for T where T::Assoc: SomeBound {
    type DecAssoc = T::Assoc;
}

(The idea is to get all your bounds into places that are "elaborated", like supertrait bounds and associated type bounds.)

But I don't know a way to do this with GATs yet, where you'd need some higher-ranked-over-types bound...

Self: for<Arg> Plain<Assoc<Arg> = <Self as Decorated>::AssocDec<Arg>>
// or
Self: Plain<for<Arg> Assoc<Arg> = <Self as Decorated>::AssocDec<Arg>>

...or an altogether different approach.

(Moreover it looks like you'd actually need for<Arg: MeetingSomeBound> for such an approach in your case, and conditional higher-ranked bounds are something else we also don't have yet.)

1 Like

Very interesting on the implied bounds. I tried making the flavored versions as their own independent traits with the plain version as a supertrait, but didn't remove the implied bound on the plain named type.

[playground]

The result was interesting, but (I guess) not unexpected. It definitely shows why it's recommended not to introduce trait bounds before they are needed.

That for<Type: Bound> syntax is exactly what I wish was available.

trait NameType
where 
    for<F: Float> Self::NamedType<F>: FloatOps,
    for<I: Int> Self::NamedType<I>: IntOps,
{
    type NamedType<E: Element>;
}