Behavioural constraints on generic types

I have a situation where I wish to provide a struct field of a generic type. How that field is implemented is up to the user of the struct. However, I wish to require the user to consider that the type must consider something. Here's the approach I've taken:

pub trait MandatoryCommands {
    fn next_event() -> Self;
}

pub struct CommandRequest<C: MandatoryCommands> {
    pub command: C,
}

Therefore, it is on the user to consider what type of C represents a next_event command.

The next_event function is not expected to be called from anywhere (at least presently). Its entire existence is to ensure that the particular command has been considered. Here's a sample implementation:

        enum Command {
            NextEvent,
            SomeOtherCommand,
        }

        impl MandatoryCommands for Command {
            fn next_event() -> Self {
                Command::NextEvent
            }
        }

The linker should remove the implementation to next_event given the lack of use. So, goal achieved in terms of the user having to consider this command in their enumeration. However, I'm wondering if there's a better way?

I could also wrap all commands with something like the following:

pub enum Command<C> {
    NextEvent,
    User(C),
}

...but then I must consider custom serialisation and it is a level of indirection on the user.

Thoughts? Thanks.

If next_event() is never actually used and you just want to make people opt-in to some specific behaviour, why not make MandatoryCommands a "marker trait"?

/// By implementing this trait you guarantee that you have considered
/// X, Y, and Z.
trait MandatoryCommands {}

Otherwise if what you are really trying to express is some association between your C: MandatoryCommands and some other type, you can use an associated type.

trait MandatoryCommands {
  type Command;
}

This requires that any C: MandatoryCommands has a C::Command type we can talk about (e.g. fn do_stuff<C>() where C: MandatoryCommands, C::Command: Clone).

1 Like

Thanks. The Marker trait sounds reasonable. It'd be nice to express:

            type NextEventCommand = Command::NextEvent;

....but NextEvent is a variant, not a type so I can't do that.

1 Like

For something like that I would normally make the Command::NextEvent variant contain a single NextEvent field.

enum Command {
  NextEvent(NextEvent),
  ...
}

struct NextEvent { ... }

From there, you can write your where clause as only accepting some type, C, where C's NextEventCommand is a NextEvent.

fn do_stuff<C>(...)
where
  C: MandatoryCommand<NextEventCommand = NextEvent>,
{
  ...
}

Although maybe more context is needed. Requiring that an associated item is a particular enum variant is normally not what you want to do, and comes about when you mix up types and values.

2 Likes

Thanks for the follow up. I think the marker trait is sufficient. If the user is determined to ignore its contract then it’s on them. And there may be valid reasons for them to do so.

Here’s the full project for more context: GitHub - titanclass/flip-flop-protocol

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.