A project I'm currently working on has a need to be able to search through text using several search modes, any (fixed) encoding width, and any endianness. To achieve this, I've settled on using a family of searcher structs that all implement the same trait, and take the encoding width and endianness as type parameters:
trait Searcher { /* … */ }
// `PrimInt` is from the `num-traits` crate; `ByteOrder` from the `byteorder` crate.
pub struct CodepointSearcher<N: PrimInt, E: ByteOrder> { /* … */}
impl<N: PrimInt, E: ByteOrder> Searcher for CodepointSearcher<N, E> { /* … */ }
pub struct RelativeSearcher<N: PrimInt, E: ByteOrder> { /* … */}
impl<N: PrimInt, E: ByteOrder> Searcher for RelativeSearcher<N, E> { /* … */ }
pub struct FormationSearcher<N: PrimInt, E: ByteOrder> { /* … */}
impl<N: PrimInt, E: ByteOrder> Searcher for FormationSearcher<N, E> { /* … */ }
I have to select between these at runtime, depending on the options the user picks – which I figure is most reasonable to handle using dynamic dispatch via a trait object. Were it a choice between a couple of types, you'd simply do this:
let polymorphic: Box<dyn Trait> = match option {
Options::One => Box::new(TypeOne::new()) as Box<dyn Trait>,
Options::Two => Box::new(TypeTwo::new()) as Box<dyn Trait>,
Options::Three => Box::new(TypeThree::new()) as Box<dyn Trait>
};
Now, being a close-to-the-metal system programming language, Rust doesn't have types as first-class citizens – but imagining that it did, here's what I'd like to do:
let SearcherType = match mode {
Mode::Codepoint => CodepointSearcher,
Mode::Relative => RelativeSearcher,
Mode::Formation => FormationSearcher
};
let IntType = match width {
EncodingWidth::Bits8 => u8,
EncodingWidth::Bits16 => u16,
EncodingWidth::Bits32 => u32
};
let EndiannessType = match endianness {
Endianness::Big => BigEndian,
Endianness::Little => LittleEndian,
Endianness::Native => NativeEndian
};
let searcher: Box<dyn Searcher> =
SearcherType::<IntType, EndiannessType>::new(/* … */);
But that's not possible, of course, so you'd have to exhaustively specify every combination as a match arm (or use nested match statements, but the effect is the same):
let searcher = match (mode, width, endianness) {
(Mode::Codepoint, EncodingWidth::Bits8, Endianness::Big) => Box::new(
CodepointSearcher::<u8, BigEndian>::new(/* … */)
) as Box<dyn Searcher>,
(Mode::Codepoint, EncodingWidth::Bits8, Endianness::Little) => Box::new(
CodepointSearcher::<u8, LittleEndian>::new(/* … */)
) as Box<dyn Searcher>,
// …
};
Here's the problem, however: Since I have three types and two sets of three possible type arguments, that works out to 3 × 3 × 3 = 27 different combinations, and… though I could type those out and sweep 'em under the rug into their own module, that's still decidedly anti-DRY.
Which brings me to my question:
- Could one write a macro that generates this match statement? If so, how? It appears to potentially be possible to write a macro that generates match arms, but highly convoluted…
- Or, perhaps preferably, is there a better way to select between a large number of type-type argument combinations?
(Here's a skeleton in Playground with these particular components: Link!)