Better way to pass trait with no reference to self

Hello,

Let say I have the following trait :

pub trait Driver: Debug + Send {
    fn make_packet(command: &Command) -> Result<Packet, DriverError>
    where
        Self: Sized;

    fn try_parse(data: &[u8]) -> ParseResult
    where
        Self: Sized;
}

The idea is that I want to be able to have different "Driver" but they have no need for a internal state so both function do not use self.

Now I have another trait called Port that abstract reading some byte and using the Driver to parse those byte. I would write it like so:

pub trait PortIn: Read + Debug {
    fn try_read_notification<D: Driver>(&mut self) -> PortTryRead {
         // the actual implementation is irrelevant.
    }
}

This has the advantage that I can just indicate to the function which driver to pass as a type indication like so : port.try_read_notification::<driver::Type1>().

This is fine, my issue comes because I have another trait called Task with the following:

pub trait Task: Debug + Send {
    fn execute(&mut self);
    fn name(&self) -> &str;
}

Notice how execute does not have any type parameter. But I want to be able to choose which driver and port to use withing this function.
This mean I end up having to do something very awkward where I store an object that implement Driver in my struct just to keep the trait information:

#[derive(Debug)]
pub struct ReaderTask<D: Driver + Send, P: PortIn + Send> {
    pub chan: flume::Sender<Notification>,
    pub port: P,
    pub _driver: D,
}

impl<D: Driver + Send, P: PortIn + Send> Task for ReaderTask<D, P> {
    fn execute(&mut self) {
        let res = self.port.try_read_notification::<D>();
        // The rest of the code is irrelevant
    }
}

Notice how _driver is never actually used.

Is there a better way to do this ? The only ways I could think of were to either modify the trait for Task to include a type parameter, but this is an issue because Task is intended to be "generic", or to have a specific implementation of the Task for each combo of driver+port, which would mean creating a lot of variation of the task.

Is there a way for me to pass the trait information to the implementation of Task without having to keep a object that implement Driver in my task ?

You can change the type of _driver to PhantomData<fn() -> D> to keep the type parameter D used while not having to own an actual instance of the type.

2 Likes

Thanks, this is working :slight_smile: .
I saw PhantomData ( happy halloween :ghost: ) while searching for a solution but I couldn't use it properly (I was doing PhantomData<D>). What is <fn() -> D> does ? It is the first time I come across this notation.

Your current implementation doesn’t need this, but are you sure future ones won’t need configuration flags or similar?

My intuition would be to store a driver in your struct, and then actually use it to make the trait calls: Storing and referencing zero-sized types is cheap.

2 Likes

PhantomData<D> would be fine, too. However, PhantomData<D> causes the compiler to pretend that your type holds an instance of D, which needlessly restricts e.g. Send and Sync impls, among others. But it's not "improper" or "incorrect".

There's nothing special about it: it's just the type of function pointers taking no arguments and returning a value of type D. This is a well-known trick.

3 Likes