Messy generic type dependencies

Suppose I have a crate that specifies, say n traits and then has a type that depends on k generic types, each of which implements various subsets of the traits.

Do I really have to define these types as

type<T1:S1+S2+....+Sp1, T2:S1+S2+...+Sp2, ... , Tl:...>

This becomes very very messy if more then say 4 traits or so are involved. Moreover this give the same messy style one all type that depend on this type, which escalates quickly throughout the code base.

What is best practice here?

Don't put trait bounds on types unless it affects the layout of types or is needed in Drop, this should allieviate most concerns.

2 Likes

AFAIK, if I need the trait functions in the messy type there is no ther way. Or is that incorrect? Maybe I misunderstand your point. Do you suggest the the case I scatched can not appear or there is always a way to circumvent it?

I'd consider typing out that many traits several times to be a code smell anyway. If you continuously need functionality from multiple traits, shouldn't the bounds be combined into a compound requirement instead? Something like

trait EverythingDoer: DoStuff + DoOther + DoSomethingElse {}

impl<T> EverythingDoer for T
    where T: DoStuff + DoOther + DoSomethingElse
{}

Then you can just use the EverythingDoer bound instead of listing all of its supertraits.

3 Likes

Can you show an example of some real code that has this problem?

Its still messy if every type only implemts one trait. I just wrote the example as general as possible. Suppose type depends on say 100 other types, which recurses through all of code base

I think the question is general enough without an concrete incarnation.

I don't really understand what you mean by that. Why would you want to apply multiple trait bounds when one suffices?

You might have something like

type A<T1,... T1000>

which is very messy, because every other type that depends on A has to write the same very long type header

I think if you plan to make type which implements hundred traits you should just stop writing the code and go to think for a while.
But answering the question, the best practice to use where statement to define contraints.

impl <T1, T2> Struct<T1,T2> 
where 
    T1 : Trait1 + Trait2 + ... + TraitN,
    T2 : Trait10 + Trait20 + ... + TraitNN
{
    //impl your messy code
}
2 Likes

Well then that's not about the number of trait bounds but the number of type parameters. And yes, if you want to parametrize a generic type over N type parameters, there's no way around specifying those N type parameters. If you show me some real, non-autogenerated code where a type has 100 parameters, I'll try to suggest a way to get down the arity of the type parameters to a reasonable number. I don't think it's possible to offer such advice completely in the abstract.

2 Likes

I really don't understand this reasoning. Do you want to bound the theory of type systems to a constant amount of generic types? Rust is turing complete. Maybe the purpose of the entire project is just to have code with 100 trait dependency.

Sorry I think I get answers from a developer perspective to an abstract level question.

Ok. "There is no way around that". Thats a simple answer not twisted by additional concerns that are out of scope. Thanks.

I believe not, however "how to write nice practical code by hand" does not really belong in the realm of theoretical questions about type-level functions of arbitrarily high arity.

So, yes, in practice, code already gets very hard to read, write, and manage if the number of type parameters reaches into the 10s. So if you need advice about easily writing generic code, you should probably draw the limit at a reasonably low upper bound that caters to humans who write and read said code.

If you want to discuss the issue of generics without a finite upper bound on the number of type parameters, you already left practical considerations behind, because it's borderline impossible to ever make such code humanly comprehensible. It might still provide valuable results or serve as a clever mental exercise, however asking to make it easy to write and/or read is just not an appropriate or relevant question in this case.

Ok fair enough. I was just hoping that Rust, since its type system is arbitraily expressable (you could even write programs entirely on the level of the type system) already came up with a cleaver way to make this more human readable.

Or not even hoping, but I just wanted to rule out that there is a "standard way" that I'm just not aware of yet.

Maybe this is what you need (code under spoiler):

Code
trait TypeCollection {
    type T1;
    type T2;
}

struct CollectionOne();
struct CollectionTwo();

impl TypeCollection for CollectionOne {
    type T1 = String;
    type T2 = String;
}

impl TypeCollection for CollectionTwo {
    type T1 = i32;
    type T2 = i32;
}

struct S<T>
   where T : TypeCollection 
{
    t1 : T::T1,
    t2 : T::T2,
}

fn main ()
{
    let s1 = S::<CollectionOne>{ t1 : String::from("hello"), t2 : String::from("world") };
    let s2 = S::<CollectionTwo>{ t1 : 13, t2 : 666 };
    
    println!("{} {}", s1.t1, s1.t2);
    println!("{} {}", s2.t1, s2.t2);
}

1 Like

That is definitely a good step forward, mostly because one does not need to change the signature in any type that uses S anymore, whenever the number of generic types changes. So the generic types become dynamic by encapsulation in T.

Of course you're free to create type collections of any size as you want. Unfortunately there are no C++ Variadic Templates analogue in Rust as far as I know so you should write all N types in collection by hand and not way to iterate over the types (it's hard to imagine why you need it until const generic is not in rust but...)

But now you have to pass one type-parameter to every generic-struct and able to wite code a bit cleaner)