Rust macro for enum variant pseudo-subtyping
Getting closer to treating enum variants like types through the use of simple generative macros.
One of the many cool things about rust is how flexible enums are. Mixing c-style, tuple, and struct variants in the same enum is a great way to give you flexibility over your models. But there are times when I want to nest enums to get a sub-and-super-class-like relationship. For example, if I’m modeling events, and I want to give all events some context (id, timestamp, key, version) in addition to their data, I might do something like this.
struct SuperEvent {
id: u64,
version: u64,
utc_ts: u64,
inner: Event
}
enum Event {
MouseClick(MouseClickEvent),
MouseMove(MouseMoveEvent),
MouseOver(MouseOverEvent),
}
struct MouseClickEvent {/*...*/}
struct MouseMoveEvent {/*...*/}
struct MouseOverEvent {/*...*/}
So far, so good. But if we want to add a trait that is implemented for all events,
there’s no way to tell rust that all variants of an Event
must implement a trait.
trait EventTrait {
fn is_valid(&self) -> bool;
}
impl EventTrait for MouseClickEvent {
fn is_valid(&self) -> bool {
true
}
}
impl EventTrait for MouseMoveEvent {
fn is_valid(&self) -> bool {
true
}
}
impl EventTrait for MouseOverEvent {
fn is_valid(&self) -> bool {
true
}
}
We can implement the trait just fine, but if we really want to treat each event as a sub-class of the super-event
(side note: please excuse my Java terminology, still got a bad case of Java Brain) then we have to proxy each variant
when we implement the trait for the SuperEvent
.
impl EventTrait for SuperEvent {
fn is_valid(&self) -> bool {
match self.inner {
Event::MouseClick(e) => e.is_valid(),
Event::MouseMove(e) => e.is_valid(),
Event::MouseOver(e) => e.is_valid(),
}
}
}
This gets worse the more methods you define on the trait, and the more enums you add to event.
Imagine if you added the methods get_origin
, get_target
, get_current_target
, and so on.
And you added the events WindowEvent
, ResizeEvent
and so on. For each method, you have to add match block.
For each event, you have to add a line to each match block. In addition to being verbose,
doing this makes it easy to miss one.
There are two ways to make this easier.
-
Drop the fields down a level, and give each event ownership over
id
,version
, and so on, exposing them via trait methods. The drawback to this is that y our data model starts to leak into whatever uses the events by requiringdyn
trait functions, generics, and probablyBox
-ing up the values. -
Solve for the repetition problem by using macros to generate the enum matching statements. Like this:
#[macro_export]
macro_rules! match_and_run {
( $event:expr, $name:ident $( , $arg:ident )* ) => {
match &$event {
Event::MouseClick(o) => o.$name($($arg),*),
Event::MouseMove(o) => o.$name($($arg),*),
Event::MouseOver(o) => o.$name($($arg),*),
}
};
}
impl EventTrait for SuperEvent {
fn is_valid(&self) -> bool {
match_and_run!(self.inner, is_valid)
}
}
Now each time you add a new trait, you add it for each event, and once for the super event, and add a single line for the match statement in the macro.