Reputation: 12406
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?
Upvotes: 0
Views: 601
Reputation: 299999
There are multiple ways to pass/specify generic parameters to a trait:
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.
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:
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:
char
or str
can be appended to String
; but not i32
.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.
Upvotes: 2
Reputation: 117771
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;
// ...
}
Upvotes: 2