Ben Vogt

Rust macro for enum variant pseudo-subtyping

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.

  1. 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 requiring dyn trait functions, generics, and probably Box-ing up the values.

  2. 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.

programming | rust
2020-10-14