Implementors of a trait, defining which typestate combinations are possible?

Hello,

Say I have a concept of a ConsumerQueueStrategy... that is, that all consumers either pull from the same queue (Mono) or they each get their own queue (Multi)

I currently have a trait Message: Serialize + Deserialize that has an associated const MONO: bool; so that I know at compile time which strategy the consumers must follow.

But the API that I build from this currently has methods for both the true and false cases... there are methods that should only be available for one strategy.

Of course, typestate is the answer here.

But can I somehow have implementors of Message also be able to define which struct of the typestate trait ConsumerQueueStrategy should apply,

Such that in the struct MessageProducer<MyMessage, Mono>

Users can't construct a MessageProducer with the wrong typestate configuration, since MyMessage doesn't allow it?

I'm thinking something like this

trait Message {
    type ConsumerQueStrategy: ConsumerQueueStrategy; 
}

struct Mono;
impl ConsumerQueueStrategy for Mono {}

struct Multi;
impl ConsumerQueueStrategy for Multi {}

trait ConsumerQueueStrategy {}

But i don't think the associated type on it's own is enough. Maybe Message should come with a generic, but then i still don't know how to force users to only use one particular struct of the generic

I'm not sure I understand what would be bad about allowing message types to be generic. Each of MyMessage<Mono> and MyMessage<Multi>, or even MyMessage<AvianProtocol>, are all still distinct types with only one possible strategy each, so it shouldn't be ambiguous with an associated type.

You could use a marker trait to narrow down which struct(s) can be used

The problem is say that I design a Message that is only intended to be used with Mono... So one crate correctly uses MessageProducer<MyMessage<Mono>>

but then the consumer crate mistakenly uses MessageConsumer<MyMessage<Multi>> this is a bug, I and want to avoid this situation

Oh sweet! Do you mean something like this?

pub struct MyMessage<CQS: ConsumerQueueStrategy + OnlyMono>

And therefore the Mono struct would be the only one that satisfies it?

Wow, and then I could leave out OnlyMono/OnlyMulti` which indicates that it could be used with either, that is some cool stuff!

Right, I see what you mean now. Having marker traits like that is a way of restricting it further (you can think of traits as constraints). You can also look into sealed traits if you want to prevent more types from implementing certain traits.

However, one could also argue that a generic message type wouldn't or shouldn't be generic if it didn't support more than one strategy. :slightly_smiling_face:

Yes, but I'm using generics not for generic's sake, but simply for the typestate pattern.

Well, with only one possible state. If you don't expect it to ever have more than one state, you wouldn't have to have it at all. Like having a function that takes a parameter with only one possible value. One would ask oneself if it's even necessary to have that parameter, unless more values will be allowed in the future.

It can of course be done to communicate something more than a type variant, so I'm asking to understand the purpose.

But MyMessage is only one implementation, someone else may make MyMessage2 and they may want a different state!

... the fact that MyMessage has a generic at all, is only to communicate to MessageProducer (and MessageConsumer) which APIs to make available.

I feel like we are talking around each other a bit, so forgive me if I seem obtuse. Here's an example of what I mean.

If we take this trait, it can be implemented without any generic:

impl Message for MyMessage {
    type ConsumerQueStrategy = Mono; 
}

// With any strategy
impl<M: Message> MessagePosterThing<M> {
    fn post(&mut self, message: M) {
        // Use M::ConsumerQueStrategy here somehow 
    }
}

// Only with Mono
impl<M: Message<ConsumerQueStrategy=Mono>> MessagePosterThing<M> {
    fn post_mono(&mut self, message: M) {
        // Do Mono stuff
    }
}

If someone makes a second message type, they can make it either generic or not and it will still work the same. If you instead want to prevent anyone from implementing certain traits, you can use the sealed trait trick to limit them to certain types.

4 Likes

Oh damn!!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.