Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I avoid PhantomData in this struct definition?

Tags:

rust

traits

I have a trait that looks something like this:

trait Handler<C> {
    fn handle(&self, msg: &Message, connection: &mut C);
}

Instances are supposed to be chained like you would chain middlewares for HTTP handlers:

let handler = FirstHandler {
     next: SecondHandler {
         next: FinalHandler {},
     },
};

Each handler type can impose additional constraints on the type C:

trait ConnectionThatWorksWithFirstHandler {
    ...
}

struct FirstHandler<C: ConnectionThatWorksWithFirstHandler, H: Handler<C>> {
    next: H,
    _phantom: PhantomData<C>,
}

As you can see here, I need a PhantomData<C> to avoid error E0392 (parameter C is never used). However, PhantomData is semantically wrong because the handlers are not holding instances of C. This is ugly. For example, I manually have to provide the correct Sync/Send trait implementations:

unsafe impl<C: ConnectionThatWorksWithFirstHandler, H: Handler<C>> Send for Handler<C, H> where H: Send {}
unsafe impl<C: ConnectionThatWorksWithFirstHandler, H: Handler<C>> Sync for Handler<C, H> where H: Sync {}

The auto trait implementations would have an additional where C: Send/Sync bound which is not appropriate here.

Is there an alternative to PhantomData that allows me to encode the relation between FirstHandler<C> and C such that the Rust compiler is happy and I don't need more unsafe code?

I'm not looking for associated types. The handler trait and its implementors are defined in a library, and the concrete type for C is defined in the application consuming the library, so the concrete type C cannot be defined by the handlers' trait implementations.

The idea with this design is to allow the chain of handlers to accumulate all the trait bounds for C that are required in the handler chain, so that when I have the handler variable as shown in the second snippet, then the implied trait bound is C: ConnectionThatWorksWithFirstHandler + ConnectionThatWorksWithSecondHandler + ConnectionThatWorksWithFinalHandler.

like image 607
Stefan Majewsky Avatar asked Sep 05 '18 14:09

Stefan Majewsky


People also ask

Why is PhantomData needed?

PhantomData consumes no space, but simulates a field of the given type for the purpose of static analysis. This was deemed to be less error-prone than explicitly telling the type-system the kind of variance that you want, while also providing other useful things such as the information needed by drop check.

What is PhantomData?

pub struct PhantomData<T> Sized; Zero-sized type used to mark things that “act like” they own a T . Adding a PhantomData<T> field to your type tells the compiler that your type acts as though it stores a value of type T , even though it doesn't really. This information is used when computing certain safety properties.


1 Answers

There is no need to enforce the constraints on the inner handler at the definition of the struct. You can delay them until you implement the Handler trait for FirstHandler.

trait Handler<C> {
    fn handle(&self, msg: &Message, connection: &mut C);
}

struct FirstHandler<H> {
    next: H
}

impl<C, H> Handler<C> for FirstHandler<H>
where
    H: Handler<C>,
    C: ConnectionThatWorksWithFirstHandler,
{
    fn handle(&self, msg: &Message, connection: &mut C) {
        //...
    }
}
like image 82
Markus Klein Avatar answered Oct 21 '22 02:10

Markus Klein