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
.
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.
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.
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) {
//...
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With