Home > other >  Taming large sets of Rust generic parameters
Taming large sets of Rust generic parameters

Time:09-30

I have a trait that does work on data which is bounded in several dimensions, so I create it with const generics allowing me to define those bounds:

trait Transmogrifier<const N: usize, const W: usize, const B: usize> {
    // ...
}

Then, I have another trait which does some other work, which itself uses a Transmogrifier, so I've got to create this fairly unpleasant looking trait:

trait Foo<T: Transmogrifier<N, W, B>, const N: usize, const W: usize, const B: usize> {
    fn init(transmogrifier: T) -> Self;
    // ...
}

I don't need N, W, and B in Foo implementations, but the compiler complains if I don't add them in there, so meh.

Now, though, I've determined that I need to extend the Transmogrifier to also take a couple of ancillary traits, that provide different ways of doing some of the transmogrifier's operations, which themselves use some of the const generics for their own purposes, leaving me with this absolute horror show:

trait Spindler<const N: usize> {
    // ...
}

trait Mutilator<const W: usize> {
    // ...
}

trait Transmogrifier<S: Spindler<N>, M: Mutilator<W>, const N: usize, const W: usize, const B: usize> {
    // ...
}

trait Foo<T: Transmogrifier<S, M, N, W, B>, S: Spindler<N>, M: Mutilator<W>, const N: usize, const W: usize, const B: usize> {
    fn init(transmogrifier: T) -> Self;
    // ...
}

At this point, all the duplication and lengthy lists of capital letters is doing my head in, and I'm starting to think I've made a terrible design blunder and there's a different way of doing this. Is there a better way of doing this, a syntax shortcut I haven't found, or is this just how it's going to be, and I've just got to go limp and think of Clippy?

CodePudding user response:

You can use a simplified proxy trait with a blanket implementation:

trait TransmogrifierProxy {
    // Here go the methods you *actually* need in Foo.
}

impl<S: Spindler<N>, M: Mutilator<W>, const N: usize, const W: usize, const B: usize>
TransmogrifierProxy for Transmogrifier<S, M, N, W, B> {
    // Implement methods you need using Transmogrifier...
}

Then Foo simplifies to:

trait Foo<T: TransmogrifierProxy> {
    fn init(transmogrifier: T) -> Self;
    // ...
}

CodePudding user response:

There are multiple ways to pass/specify generic parameters to a trait:

  • As a generic parameter, or an associated type or constant, or a function parameter.
  • As a single parameter, or as a bundle.

You have chosen, at the moment, to pass each parameter as a singular generic parameter -- an easy reflex -- but maybe any of the other 5 modes would be more sensible.

Generic Parameter, or Associated Item, or Function Parameter?

At the trait level, the choice hinges on whether a single type should implement the trait for 1 or several values of the given parameter:

  • Single value: associated item.
  • Multiple values: generic parameter.

Having a generic function on a trait has different consequences: it's more flexible, but precludes using the trait in dyn contexts.

Examples in the standard library:

  • Iterator uses an associated Item type: there is a single type of elements yielded by a given collection.
  • Extend uses:
    • A generic parameter: a single type may be extended in different ways: char or str can be appended to String; but not i32.
    • A generic function: any iterator yielding the right items works out.

Single Parameter or Bundle of Parameters?

If multiple parameters are linked together, it may make sense instead to bundle them together.

For example, recently I created a trait with several methods, and each method was given an additional user-defined parameter -- for the user to pass whatever supplementary data they wish to. I could define the trait as taking 1 generic parameter per method, but as you noted this gets unwieldy very quickly:

trait Foo<A, B, C> {
    fn a(&self, x: String, user: A);
    fn b(&self, y: i64, user: B);
    fn c(&self, z: Option<&str>, user: C);
}

fn fooing<A, B, C>(foo: &impl Foo<A, B, C>);

Instead, the trait is parameterized by a single type, itself implementing a trait:

trait Fooer {
    type A = ();
    type B = ();
    type C = ();
}

trait Foo<F: Fooer> {
    fn a(&self, x: String, user: F::A);
    fn b(&self, y: i64, user: F::B);
    fn c(&self, z: Option<&str>, user: F::C);
}

fn fooing<F: Fooer>(foo: &impl Foo<F>);

The user is still allowed to customize the extra data passed in each method, but the boilerplate by anything manipulating Foo is significantly reduced.

  • Related