Modelling question

Hi there - I have a bunch of Events. Each event has the same envelope (timestamp, unique ID etc.) but the detail of the event differs.

I've modelled this as an EventDetail enum and an Event struct whose detail field is of type EventDetail.

This works well - type safe, exhaustive matches etc. but it is also really painful to have factory methods for the events because the Event struct isn't typed based on the EventDetail so every consumer of an event, even if the consumer KNOWS the type of detail needs to match on the detail.

#[derive(Debug)]
enum EventDetail {
    Detail1 { a: usize },
    Detail2 { b: usize },
}
#[derive(Debug)]
struct Event {
    id: usize,
    detail: EventDetail,
}

fn main() {
    let detail1: Event = create_detail_1();
    dbg!(&detail1);
    if let EventDetail::Detail1 { a } = detail1.detail {
        println!("detail 1: {}", &a);
    } else {
        panic!("gah")
    }

    let detail2: Event = create_detail_2();
    dbg!(&detail2);
    if let EventDetail::Detail2 { b } = detail2.detail {
        println!("detail 2: {}", &b);
    } else {
        panic!("gah")
    }
}

fn create_detail_1() -> Event {
    Event {
        id: 1,
        detail: EventDetail::Detail1 { a: 1 },
    }
}

fn create_detail_2() -> Event {
    Event {
        id: 2,
        detail: EventDetail::Detail2 { b: 2 },
    }
}

On the other hand, I could use generics, which (I think) won't allow me exhaustive matching, but then again I won't need it because dispatch is based on the type of detail.

This provides a much nicer development experience.

trait Detail {}
#[derive(Debug)]
struct Event<T: Detail> {
    id: usize,
    detail: T,
}

#[derive(Debug)]
struct Detail1 {
    a: usize,
}
impl Detail for Detail1 {}

#[derive(Debug)]
struct Detail2 {
    b: usize,
}
impl Detail for Detail2 {}

fn main() {
    let detail1: Event<Detail1> = create_detail_1();
    println!("detail 1: {}", &detail1.detail.a);
    dbg!(&detail1);
    let detail2: Event<Detail2> = create_detail_2();
    dbg!(&detail2);
    println!("detail 2: {}", &detail2.detail.b);
}

fn create_detail_1() -> Event<Detail1> {
    Event {
        id: 1,
        detail: Detail1 { a: 1 },
    }
}

fn create_detail_2() -> Event<Detail2> {
    Event {
        id: 2,
        detail: Detail2 { b: 2 },
    }
}

My question is - enums seem to be preferred over generics for modelling this sort of thing, but they are much more painful when there are a lot of different types (I have hundreds of different types of events).

What are your thoughts?

You can store events using Box<dyn Any> and downcast them when needed. Or use anymap.

1 Like

I would use the enum and provide helper methods to ease use for callers: unwrap_a() -> A and unwrap_b() -> B

2 Likes

Not really. They aren't "preferred". Enums and generics solve different problems. If you need to differentiate between events based on their type at compile time, then do use generics.

2 Likes

I would probably write this as something like:

(untested)

trait Detail: Any + Debug {
    fn as_any(&self)->&dyn Any { self }
    fn as_any_mut(&mut self)->&mut dyn Any { self }

    // Other useful methods here…
}

#[derive(Debug)]
struct Event<T: Detail + ?Sized> {
    id: usize,
    detail: T,
}

This will let you work with both Event<SpecificType> when you know what you’re dealing with and Box<Event<dyn Detail>> when you’re just dealing with the wrappers generically.

2 Likes

thanks. Will that allow me to get to:

{
    do_it(detail1);
    do_it(detail2);
}

fn do_it<T: Detail>(event: Event<T>) {
    match event.detail {
        Detail1 { a } => println!("matched detail1: {}", a),
        Detail2 { b } => println!("matched detail2: {}", b),
    }
}

I guess that replacing do_it with a trait is the more idiomatic way in Rust, but I'm typically only interested in a low percentage of events and there isn't an equivalent of _ => () in traits AFAIK. This is all starting to feel a little like Java's double dispatch/visitor pattern which makes me nervous...

No, of course you can't match on a type parameter. How would the compiler know it's the concrete enum you are expecting, for example?

Anyway, trying to dispatch on the concrete type of a generic (or a trait object, or a superclass, …) would be an anti-pattern regardless. If you move your logic into generics, then you should be performing the type dispatch by calling trait methods, and not by manually checking the type.

1 Like

You can’t use match, and don’t get exhaustiveness checking, but you can do something similar:

(untested)

{
    do_it(detail1);
    do_it(detail2);
}

fn do_it<T: Detail+?Sized>(event: &Event<T>) {
    if let Some(a) = event.detail.as_any().downcast_ref::<Detail1>() {
        println!("matched detail1: {}", a);
    } else if let Some(b) = event.detail.as_any().downcast_ref::<Detail2>() [
        println!("matched detail2: {}", b);
    } else {
        panic!(“Unexpected payload: {:?}”, event.detail);
    }
}

You can always define a default implementation of the trait method that does nothing, and then override it in the trait implementations that should do something interesting.

2 Likes

I understand we can't at the moment, I don't understand why you can't, or why you say "of course"? It's possible at runtime to do it using the downcast trick so I'm not sure I understand the obviousness of why it isn't done?

thanks :slight_smile:

Because pattern matching isn't downcasting. Pattern matching is the structural decomposition of a known type.

1 Like