Trait associated type with more generics than the trait definition

It's the first time I get into this situation and I don't know what's the best way to go. It sounds like this should be solvable. But unsure what's the direction TBH.

So far, imagine we have the following code:

pub trait CoordinateOps<F: TraitA>: Sized {
    type Config: Sized + Clone;

    // fn....
    // None of the functions relies on any other generic than `F`.
}

Now I have 2 different implementors for this trait.
Both share the generic F, but the problem lies within the associated type.

impl<F: TraitA> CoordinateOps<F> for ModInt<F> {
    // This associated type has generics which aren't related to the trait nor the struct that implements them.
    type Config = FFAChip<F, GenericB, GenericC>;
}

The naive thing of trying:

impl<F: TraitA, CG: GenericB<F>, FFAP: GenericC> CoordinateOps<F> for ModInt<F> {
    type Config = FFAChip<F, CG, FFAP>;
    // fn....
}

Doesn't work ofc as the type parameter CG is not constrained by the impl trait, self type, or predicates.

Is there any standard way to workarround this?
Thanks in advance :slight_smile:

You can never implement the same trait more than once for any given type. If you want two different impls, it means you need two different traits, i.e., you need to either put an additional generic type parameter on the trait:

pub trait CoordinateOps<F: TraitA, CG>: Sized {
    type Config: Sized + Clone;
}

and then impl different traits distinguished by their second parameter:

impl<F: TraitA, CG: GenericB<F>, ...> CoordinateOps<F, CG> for ModInt<F> {
    type Config = FFAChip<F, CG, ...>;
}

Or explicitly allow the associated type to be generic (GAT):

pub trait CoordinateOps<F: TraitA>: Sized {
    type Config<T>: Sized + Clone;
}
2 Likes

This sounds quite bad to me, as one of the implementors doesn't need these generics. Only one of the implementors needs more stuff than TraitA.

Ye, as said above, this is not valid for other implementors which do not rely on CG or FFAP. Maybe I didn't state this clearly enough in the initial post.. Apologies.

As for GATs, I didn't think about it though! I have the same example as always of the Iterator hardcoded in my mind such that I cannot relate it to any other thing hahaha.

But even when doing:

pub trait CoordinateOps<F: PrimeField>: Sized {
    type Config<T, R, W>: Sized + Clone;

// Then I get the same error when defining:
impl<F: PrimeField, CG: CapArithInstructions<F>, FFAP: FFAParams> CoordinateOps<F> for ModInt<F> {
    type Config = FFAChip<F, CG, FFAP>;
// ....
}

// Or when defining:
impl<F: PrimeField> CoordinateOps<F> for ModInt<F> {
    type Config = FFAChip<F, CG: GenericB<F>, FFAP: GenericC>;
// That one requires indeed `Associated Type Bounds` -> https://github.com/rust-lang/rust/issues/52662
}

Any other ideas? Or I should just give up the trait and duplicate code at this stage?

I want to emphasize that there are other implementors of this trait which will not need the extra generics that are being brought by CG and FFAP.

A generic associated type is generic, i.e., it's not a type. The whole point of a GAT is that there's no Config associated type. There's only Config<F, CG, FFAP> for some choices of types in place of F, CG and FFAP. This compiles.

In your Playground example, I notice that in the implementor code, the generics aren't still constrainted to the correct trait bounds.

impl<F: PrimeField> CoordinateOps<F> for ModInt<F> {
    type Config<CG, FFAP> = FFAChip<F, CG, FFAP>;

This complains since CG should be CapArithInstructions<F> but this is not implied anywhere. Same happens with FFAP with FFAParams.

I hope this helps to showcase the problem more. I want to have these generics only for one of the implementors. But Not for all of them as these generics are only specific for one of the trait implementors but not others.

Hence why I asked if I should give up the trait way.

In general, you shouldn't constrain type definitions with trait bounds. You should only include bounds when and where they are actually needed.

2 Likes

Non-generic associated types are outputs to be completely determined by the implemented trait and the implementing type, hence the "unconstrained" error.

If nothing else in the trait relies on a generic other than F, why is Config an associated type of the trait? (If something else relies on FFAChip<F, GenericB, GenericC>, then it relies on generics other than F.)

2 Likes

This might be my design flaw.

(If something else relies on FFAChip<F, GenericB, GenericC>, then it relies on generics other than F.)

The thing is that this is only the case for 1 implementor of the trait. But not the case for others.
So I wanted to mask this using an associated type which each implementor can change according to their needs.

There's a third possibility, which is to define different types so that they can have different impls. In particular, any generic parameters used in the type definition being implemented are considered constrained for the purpose of the associated type. For example:

impl<F: TraitA, GenericB, GenericC> CoordinateOps<F> for ModInt<F, GenericB, GenericC> {
    type Config = FFAChip<F, GenericB, GenericC>;
}

If you don't need to hold a copy of those extra generics inside ModInt, you can use a PhantomData field to make the additional parameters legal.

2 Likes

Yep, this is another good solution to OP's problem. (Although I rest my case that "same trait for same type twice" does not work, since in this case, the type changes while the trait stays the same.)

I thought about this and it's the closest to what I would like.

It brings the verbosity issue to the ModInt type which now has 3-associated generics when it only needs one.. true.. But solves the re-ussage!

I'll think about it. But so far, this is the closest to what I want. And seems I'm not crazy considering it.
In any case, I still think I missdessigned somewhere to arrive to this situation :sweat_smile:

Well, if you want the latest approach, then you are using marker types, to differentiate between kinds of behavior statically. That's by no means a design error. You might think that unit (information-less) types are useless, but types are actually "type-level values", so the identity of types does contain compile-time information (even though they don't provide any run-time information). Compile-time is run-time for the compiler.