New to Rust - Help Improving a Simple Event Simulator

Hello Rust Community!

This may be a bit of a nebulous ask, but I would appreciate some general feedback from seasoned Rust veterans. I'm fairly new to the language and implemented this basic Event Simulator as a way to improve my Rust. It's pretty simple - there are a bunch of objects that send events to each other. This is my first project working with traits. It works insofar as the small example at the end (a single object echoing events to itself), but I'm not happy with several elements. I'd appreciate any tips you're willing to offer to improve it, but I do have some specific concerns and questions below and would appreciate specific feedback about those. Small tweaks, complete redesigns.. all fair game, I just want to learn. Thanks!

  • How can I eliminate the HashMap? It's being used as a hack to allow a QueuedEvent to contain a "reference" (String key) to a SimObject. I tried references at first, but things got really ugly with lifetimes. Plus, it ultimately needs to be a mutable reference because we need to call its process_event() method (line 101), and that seems like it will break some borrowing rules.

  • You'll see I use Box<dyn SimEvent> in several places. I don't have a particular reason to useBox<> except that I couldn't figure out another way. Open to other ways of passing/holding SimEvents if there's an advantage. I like the <X: SimEvent> notation, but not sure how it would work here. See next question for possible consideration.

  • Eventually, a particular SimObject's process_event() method needs to be able to "downcast" the input SimEvent (or at least access methods/fields). Imagine ExampleEvent carried some data and had a method. How could process_event() determine that the SimEvent was an ExampleEvent and access those? I've looked into Any, which seems like it would work, but I'm not sure how to properly use it with traits and/or boxes. Are there other solutions? For example, I wish I could define ExampleObject's process_event() to take an argument that implementsSimEvent + SomeOtherTrait, but my attempts to do so cause compiler complaints about matching the trait definition.

  • The uuids in the QueuedEvents feel a little dumb - always initialized to 0, then set in EventSimulator->queue_event(). Is there a way I could get them to have a counter id that is correct at initialization?

Here's the code. Thanks so much for your time.

use std::cmp::Ordering;
use std::collections::{BinaryHeap, HashMap};

trait SimEvent {}

trait SimObject {
    fn process_event(&mut self, e: Box<dyn SimEvent>, time: u64) -> Vec<QueuedEvent>;
}

struct QueuedEvent {
    event: Box<dyn SimEvent>,
    target: String,
    delivery_time: u64,
    uuid: u64,
}
impl QueuedEvent {
    fn new(event: Box<dyn SimEvent>, target: String, delivery_time: u64) -> Self {
        Self {
            event: event,
            target: target,
            delivery_time: delivery_time,
            uuid: 0,
        }
    }
}

// stuff to make QueuedEvent orderable
impl Ord for QueuedEvent {
    fn cmp(&self, other: &Self) -> Ordering {
        if self.delivery_time != other.delivery_time {
            self.delivery_time.cmp(&other.delivery_time).reverse()
        } else {
            self.uuid.cmp(&other.delivery_time).reverse()
        }
    }
}
impl PartialEq for QueuedEvent {
    fn eq(&self, other: &Self) -> bool {
        self.uuid == other.uuid
    }
}
impl Eq for QueuedEvent {}
impl PartialOrd for QueuedEvent {
    fn partial_cmp(&self, other: &QueuedEvent) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

struct EventSimulator {
    current_time: u64,
    end_time: u64,
    uuid_counter: u64,
    queue: BinaryHeap<QueuedEvent>,
    objects: HashMap<String, Box<dyn SimObject>>,
}

impl EventSimulator {
    fn new(end_time: u64) -> Self {
        let q = BinaryHeap::new();
        let o: HashMap<String, Box<dyn SimObject>> = HashMap::new();
        Self {
            current_time: 0,
            end_time: end_time,
            uuid_counter: 0,
            queue: q,
            objects: o,
        }
    }

    fn add_object(&mut self, name: String, object: Box<dyn SimObject>) {
        self.objects.insert(name, object);
    }

    fn run(&mut self) {
        loop {
            let qe = match self.queue.pop() {
                Some(q) => q,
                None => {
                    println!(
                        "Event Queue is empty, ending simulation at time {}",
                        self.current_time
                    );
                    break;
                }
            };
            self.current_time = qe.delivery_time;
            if self.current_time >= self.end_time {
                println!(
                    "self.current_time >= self.end_time ({}>={}), ending simulation",
                    self.current_time, self.end_time
                );
                break;
            }
            let target = match self.objects.get_mut(&qe.target) {
                Some(t) => t,
                None => {
                    println!("SimObject {} not found! Exiting.", qe.target);
                    break;
                }
            };
            for returned_event in target.process_event(qe.event, self.current_time) {
                self.queue_event(returned_event);
            }
        }
    }

    fn queue_event(&mut self, mut qe: QueuedEvent) {
        qe.uuid = self.uuid_counter;
        self.uuid_counter += 1;
        self.queue.push(qe);
    }
}

// BEGIN EXAMPLE APPLICATION
struct ExampleEvent {}
impl SimEvent for ExampleEvent {}

struct ExampleObject {}
impl SimObject for ExampleObject {
    fn process_event(&mut self, e: Box<dyn SimEvent>, time: u64) -> Vec<QueuedEvent> {
        println!("Got the event at time {}", time);
        vec![QueuedEvent::new(e, String::from("test_obj_1"), time + 1)]
    }
}

fn main() {
    let mut sim = EventSimulator::new(10);
    sim.add_object(String::from("test_obj_1"), Box::new(ExampleObject {}));

    let e = Box::new(ExampleEvent {});
    let qe = QueuedEvent::new(e, String::from("test_obj_1"), 1);
    sim.queue_event(qe);

    sim.run();
}

(Playground)

Box<dyn SimEvent> is the simplest way to have an owned trait object, so it's fine to use it. If you need to reference the same object from multiple places, you can use Rc<dyn SimEvent> instead. That will possibly help you with the hashmap elimination.

For downcasting from a custom trait, you can use one of the helper crates. I'm not sure which is better.

If QueuedEvent should always have an uuid, it makes sense to pass it as a parameter to its constructor. One possibility is to have an Event that doesn't have uuid field and make QueuedEvent consist of two fields: uuid and event: Event. Then you can accept an Event as a parameter to queue_event function and construct an QueuedEvent inside the function.

Thanks @Riateche. Everything you suggested made sense. I particularly appreciate you bringing Rc to my attention. I've looked over the documentation and I think you are correct - it may be the path to eliminating the HashMap.

I'll spend more time looking at the helper crates for downcasting. Thanks again for your reply.

I think you can use an enum as an alternative to traits objects, if you know all possible implementations of SimObject. I don't know if its better but there is a certain duality between the two. Sometimes one or the other is more appropriate.

enum SimEvent {
    Example,
}

impl SimEvent {
    fn process(&mut self, sim: &mut EventSimulator, time: u64) -> Vec<QueuedEvent> {
        match *self {
            SimEvent::Example => {
                  // code from ExampleObject
            }
        }
    }
}

Thanks @mickvangelderen, that's a useful point. In this case, I don't know all the possible SimObjects - I think of the base simulator as more of a generic library.

I was able to remove the HashMap (my biggest objective) using @Riateche's suggestion of Rc (plus RefCell). This section of the Rust book was very helpful:

https://doc.rust-lang.org/book/ch15-05-interior-mutability.html

Here's another playground link in case anyone wants to see that in action. Thanks to both of you for your input. Other suggestions for improvement always welcome.

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=3ebf66ec58b52751e6f1bc742688d4df