In my project I have a trait called Processor
, which looks like this (Frame
is another trait that abstracts over arrays of numerical samples of any size):
use crate::Frame;
pub trait Processor<const NI: usize, const NO: usize> {
type Input: Frame<NI>;
type Output: Frame<NO>;
fn process(&mut self, input: Self::Input) -> Self::Output;
}
In the same module, I have a number of structs that implement Processor
, each with varying fields and behaviors. However, each one has a new
method for creating an instance. For example, here's the Map
struct along with its new
method and Processor
/From
impls:
pub struct Map<FI, FO, M, const NI: usize, const NO: usize>
where
FI: Frame<NI>,
FO: Frame<NO>,
M: FnMut(FI) -> FO,
{
pub(super) func: M,
pub(super) _marker: std::marker::PhantomData<(FI, FO)>,
}
impl<FI, FO, M, const NI: usize, const NO: usize> Map<FI, FO, M, NI, NO>
where
FI: Frame<NI>,
FO: Frame<NO>,
M: FnMut(FI) -> FO,
{
pub fn new(func: M) -> Self {
Self { func, _marker: Default::default() }
}
}
impl<FI, FO, M, const NI: usize, const NO: usize> Processor<NI, NO> for Map<FI, FO, M, NI, NO>
where
FI: Frame<NI>,
FO: Frame<NO>,
M: FnMut(FI) -> FO,
{
type Input = FI;
type Output = FO;
fn process(&mut self, input: Self::Input) -> Self::Output {
(self.func)(input)
}
}
Also in my project, I have another trait called Signal
, these are streams of Frame
s that behave similarly to the stdlib Iterator
. It makes sense to be able to "plug in" a Processor
into a Signal
to transform it; indeed, I already have such a method defined on Signal
:
pub trait Signal<const N: usize> {
type Frame: Frame<N>;
fn next(&mut self) -> Option<Self::Frame>;
fn process<P, const NO: usize>(self, processor: P) -> SigProcess<Self, P, N, NO>
where
Self: Sized,
P: Processor<N, NO, Input = Self::Frame>,
{
// This is a new `Signal` type.
SigProcess { signal: self, processor }
}
}
For ergonomics, I'd love to be able to have the user avoid having to import Map
/Processor
and dealing with its semantics, so I'm planning on adding a map
method to Signal
itself:
pub trait Signal<const N: usize> {
/* PREVIOUSLY SHOWN CODE */
fn map<FO, M, const NO: usize>(self, func: M)
-> Process<Self, Map<Self::Frame, FO, M, N, NO>, N, NO>
where
Self: Sized,
FO: Frame<NO>,
M: FnMut(Self::Frame) -> FO,
{
let processor = Map::new(func);
self.process(processor)
}
}
Whew, that was a lot of build up. Now here's the main question I'm asking: how can I programatically add these methods to the Signal
trait at compile time for every Processor
implementor I have? As can be seen, creating a Signal::map
that bakes in an instance of Map
is pretty straightforward: Map::new
requires a func: M
as input, as does Signal::map
(self
aside). But it's a LOT of boilerplate code, and it's going to be a pain to manage once I end up with the dozens of Processor
s in the final version, each of which will need to have a separate corresponding method in Signal
.
I'm at a loss as to how to approach this, given:
- The complexity with type and const generics, especially since I might have to merge or rename type/const vars.
- Not knowing how to do code reflection in Rust, or if it's even possible! I'd need to effectively be able to ask the compiler "please look in the
processors
module, get a list of all types thatimpl Processor
, and for each one: copy the signatures and generics for thenew
method and insert a new method in theSignal
trait". - If there is a way to approach this with something like procedural macros, I'd have to learn them from scratch, but I'd be willing to do so if it's possible to make this work.
Any advice would be greatly appreciated!
Aucun commentaire:
Enregistrer un commentaire