Avoiding repeating trait bounds

Hi - I need to repeat trait bounds that really shouldn't be changed.

I have a trait which has a few associated types. I have a struct which itself is generic and implements the trait and passes one of its generic parameters to the trait's associated type. It also has a member which itself is the trait.

The problem is that I am repeating the same decisions over and over, the decision of which concrete type to use for the struct, and the concretisation of the GATs in the underlying trait.

(In OOP this might be something like the Decorator pattern)

I know I'm fighting Rust, but this seems like a pretty straight forward requirement - the decision should be made in one place (the struct deciding to use usize). Am I just hitting limits of Rust's syntax?

Thanks!

playground: Rust Playground :

trait InnerTrait {
    type A;
    type B;
}

struct Thing<A, T>
where
    T: InnerTrait<A = A, B = usize>,
{
    inner_trait_impl: T,
}

impl<A, InnerTraitImpl> InnerTrait for Thing<A, InnerTraitImpl>
where
    InnerTraitImpl: InnerTrait<A = A, B = usize>, // USIZE IS REPEATED
{
    type A = A;
    type B = usize;
}

struct AnotherInnerTrait<B> {
    _b: core::marker::PhantomData<B>,
}
impl<B> InnerTrait for AnotherInnerTrait<B> {
    type A = String;
    type B = B;
}

fn main() {
    let thing = Thing::<String, AnotherInnerTrait<usize>> /* USIZE IS REPEATED */{
        inner_trait_impl: AnotherInnerTrait::<usize>{ _b: core::marker::PhantomData} /* USIZE IS REPEATED */
    };
}

/* MY IDEAL, MADE UP SYNTAX:
struct Thing<A> {
    // NEW THING
    type MySpecialisedInnerTrait = InnerTrait<A = A, B = usize>;

    inner_trait_impl: T,
}

impl<A> InnerTrait for Thing<A, MySpecialisedInnerTrait = AnotherInnerTrait<Self::MySpecialisedInnerTrait::B> {
    type A = Self::MySpecialisedInnerTrait::A;
    type B = Self::MySpecialisedInnerTrait::B; // NOT repeating usize
}
*/

I'm no expert in these things, and might not be solving your actual problem, but I managed to tweak your example code to only include usize once.

trait InnerTrait {
    type A;
    type B;
}


struct Thing<T> {
    inner_trait_impl: T,
}

impl<A, B, T> InnerTrait for Thing<T>
where T: InnerTrait<A = A, B = B> {
    type A = A;
    type B = B;
}

struct AnotherInnerTrait<B> {
    _b: core::marker::PhantomData<B>,
}
impl<B> InnerTrait for AnotherInnerTrait<B> {
    type A = String;
    type B = B;
}

fn main() {
    let thing = Thing {
        inner_trait_impl: AnotherInnerTrait::<usize> { _b: core::marker::PhantomData }
    };
}

I think (again not really an expert) that it's often better to not constrain the struct definitions, and instead do that in the impl. (I suspect someone with more expertise in these things can probably give a better solution or explain why it's not possible at all).

2 Likes

associated types and generic parameter types have different use cases.

first, if your Thing doesn't care A, no need to spell it out:

struct Thing<T>
where
    T: InnerTrait<B = usize>,
{
    inner_trait_impl: T,
}

but generally, it's common to leave out the type traits on the data type itself, but only add required bounds for individual impl blocks, or even methods.

again, you are mixing parameter types and associated types. you can rewrite it like this:

impl<InnerTraitImpl> InnerTrait for Thing<InnerTraitImpl>
where
    InnerTraitImpl: InnerTrait<B = usize>,
{
    type A = <InnerTraitImpl as InnerTrait>::A;
    type B = usize;
}

you don't need the Thing annotations, type interference will do the work. the AnotherInnerTrait however, does need to explicitly specify the generic argument

let thing = Thing {
    inner_trait_impl: AnotherInnerTrait::<usize>{ _b: core::marker::PhantomData}
};

but you can provide a "default" argument for the type parameter in this case:

struct AnotherInnerTrait<B = usize> {
    _b: core::marker::PhantomData<B>,
}

let thing = Thing {
    inner_trait_impl: AnotherInnerTrait { _b: core::marker::PhantomData}
};

but overall, you are treating the associated types like they are parameters. I don't know what the real code does, but I think there should be better alternative designs.

3 Likes

thanks @Tristan - unfortunately it is only Thing that needs to make the decision to use size.

well, I think the trait bound on the Thing affect the inference more than the "default" argument. but you can give the type checker a hint using functions type signatures too.

you can sometimes use a sub trait as a specialized version of a more general trait. I simplified your example code a little bit:

but I don't know your actual intended use case so can't make more suggestions.

2 Likes

thanks @nerditation.

The real code basically has a ValueToEntityId trait for managing the association between a Value and a bitmap containing EntityIds, where both Value and EntityId are generic.

I have a NumericValueToEntityId implementation for Values that are numerical (e.g. the Id of a Product) and a DateValueToEntityId implementation for Values that are date based (e.g. the date an order needs delivering).

DateValueToEntityId commonly wants to find EntityIds for a range of Values (e.g. all purchases ordered last year) and wants to be performant so it denormalises dates into coarser grained layers (days, weeks, months, years). Querying then works backwards, so when querying for a whole year it simply queries the denormalised/cached Year layer and each layer is itself a ValueToEntityId trait.

It looks something like the sample code (non working code follows):

trait ValueToEntityId {
  type ValueId;
  type EntityId;
  type RangeType<Self::ValueId>;

  fn lookup(&self, r: RangeType) -> HashSet<EntityId>;
}

// this wants to impl ValueToEntityId<ValueId = Into<usize>, EntityId = EntityId>
struct SimpleValueToEntityId<EntityId> {}

// this wants to impl ValueToEntityId<ValueId = MyDate, EntityId = EntityId>
struct DateToEntityId<EntityId, X> 
where {
  X: ValueToEntityId<ValueId = MyDate, EntityId = EntityId>  
  days: X,
  weeks: X,
  //...
}

// and constructed like:
DateToEntityId<ProductId, SimpleValueToEntityId> {
  days: SimpleValueToEntityId::default(),
  week: SimpleValueToEntityId::default(),
}

impl Parameterized<B> is a neat trick! Thanks

I don't have the bigger picture of your application, but from your description, I feel like for the ValueToEntityId trait, ValueId and EntityId probably should be parameter types, if your code need to specify the associated types every time it is used as trait bound.

1 Like

I went for associated types because there is only one meaningful implementation for any given combination of choices for the generic types. They could be parameter types as well.

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.