Observer patterns in Rust

Hi, I am trying to implement the observer pattern in rust for my game. It took me a while to realize that what I had in mind was actually the observer pattern, but now that I figured it out I tried to make a mock up based on refactorguru. I tried to extend this to my particular scenario, but I realized I have two problems.

  1. I would like my events to be enum structs, so that they can store the event relevant data in the event, which I then can pass on to the subscriber, instead of having the subscriber having to fulfill a trait in order to call the right functions on it.
  2. The subscribers collection should be some map (probably a hashmap) mapping some event variant to a closure that accepts the event as an input. As you can see in the example below, I want this closure to mutate a non-owned object. So I guess that will have to work with RefCells? I am not 100% clear on the details here. Any help would be much appreciated.

I have looked at the lpxxn project's implementation of the observer pattern, but I found the lack of events in their code to render the example useless to me.

This is my code:

mod publisher {
    use std::collections::HashMap;
    use std::hash::Hash;

    pub trait Publisher<Event, Subscriber>
    where
        Event: Clone + Eq + Hash,
        Subscriber: Fn(Event),
    {
        fn get_events(&self) -> &HashMap<Event, Vec<Subscriber>>;
        fn get_events_mut(&mut self) -> &mut HashMap<Event, Vec<Subscriber>>;

        fn subscribe(&mut self, event_type: Event, listener: Subscriber) {
            self.get_events().entry(event_type.clone()).or_default();
            self.get_events()
                .get_mut(&event_type)
                .unwrap()
                .push(listener);
        }
    }
}
pub struct Game {
    player_publisher: player::Publisher,
    log: Log,
}
impl Game {
    fn new() -> Self {
        let player_publisher = player::Publisher::default();
        let log = Log::default();
        player_publisher.subscribe(player::Event::PlayerJoins, |event| {
            log.log_event_subscriber(event)
        });

        Self {
            player_publisher,
            log,
        }
    }
}

#[derive(Default)]
pub struct Log(Vec<player::Event>);
impl Log {
    fn log_event_subscriber(&mut self, event: player::Event) {
        self.0.push(event);
    }
}

mod player {
    use std::collections::HashMap;

    use crate::publisher;
    type ID = usize;
    #[derive(Clone, PartialEq, Eq, Hash)]
    pub enum Event {
        PlayerJoins { id: ID },
        PlayerLeaves { id: ID },
    }
    type Subscriber = fn(Event);

    #[derive(Default)]
    pub struct Publisher {
        events: HashMap<Event, Vec<Subscriber>>,
    }
    impl publisher::Publisher<Event, Subscriber> for Publisher {
        fn get_events(&self) -> &HashMap<Event, Vec<Subscriber>> {
            &self.events
        }

        fn get_events_mut(&mut self) -> &mut HashMap<Event, Vec<Subscriber>> {
            &mut self.events
        }
    }
}

Maybe you could explain what the observer pattern is purported to solve in your game? The code you're showing is basically just a limited channel. It might be better to use a channel and either block for events in another thread or poll it periodically with try_recv()?

The observer pattern is supposed to allow me to structure the game systems around events, e.g. "player buys card" is an event that the "PlayerMoney" system would listen to, to decrease the money the player owns. The "PlayerHand" system would listen to it as well, but to update the cards in the hand of the player. etc.

I have worked a bit with RxJs in the past and I felt like that way of architecting my code would be very suitable for a system like this, since it has so many different kinds of user events and behavior coupled to those events.

I believe in Rust you would use Futures and broadcast channels in the uses cases of observables. Both of which have all* the methods to transform and filter data.

3 Likes

Also, if you are using bevy, it has an event pair (EventWriter and EventReader, see bevy/event.rs at main · bevyengine/bevy · GitHub) for message passing between systems.

It is common to see message passing, and much less common to use callbacks via closures in Rust. Some of the challenges with closures occur because erasing lifetimes is not really trivial. So, your closures tend to only have static references, which are not always useful or what you expect [1].

It's the "share by communicating, don't communicate by sharing" mantra.


  1. Boxing or reference counting might help in some cases. The trick is to avoid borrowing temporary values in your closures, or otherwise ensuring that the references cannot outlive the owner, e.g. through scope guards. ↩︎

I'm trying to design my application in Rust, but coming from Javascript, callbacks were very frequent. I could throw them around and store them anywhere I like. The more I read about how unpleasant and complicated callbacks are to use in Rust, the more I'm trying to avoid them. Can you always avoid using callbacks in rust? I heard some people using arc/mutex Box move & mut dyn fn to make it work?

You can absolutely use Box<dyn Fn()> or whatever: the problem is that this doesn't help as a callback mechanism by itself, in the sense of a function that will outlive the current call and modify existing state.

You might naively start to assume you need to use Rc<RefCell<State>> or something, then you find your callbacks are recursive, so the borrow fails, then twenty other things go wrong and you start to wonder if you really need this job that badly ...

If you just hand out a channel sender into the callback instead of state directly (or if you're implementing the event yourself, skip the callback function and just send the event), a lot of these issues often disappear. You can pass the same channel out to all the callbacks, and just read events in a loop and switch on the type.

2 Likes

wow great wisdom. will definitely keep this in mind to use message channels when possible.

Of course you can! People were writing games on platforms where callbacks were impossible because if hardware limitations yet they wrote pretty elaborate games.

The question in programming is never whether you can but whether you should.

Frankly, when I see a pile of callbacks without any structure and reason I cringe: what's the point of all that activity? To avoid planning ahead?

It doesn't help long-term anyway: you can push the rope for so long before it becomes tied in knots.

Again: it's possible to use Arc<Mutex<>> and model that architecture in Rust. But it's painful and quickly raises the question: why are writing JavaScript in Rust and endure all that pain and verbosity if you may as well just write JavaScript in, you know, JavaScript?

could you take a look at my other post and share some thoughts maybe?

I do know JS but I'm leaving it for good. I am really liking rust so far and ready to unlearn the bad habits of JS :wink:

Thought about what, exactly? You have some kind of Rust code there which doesn't look much like Rust code at all. And then you tweak it till it works. How would I know whether that's the good code or not for the task that you are trying to solve?

Usually you have to describe the task you are actually trying to solve before anyone can say whether you have invented the best solution.

Sometimes even GC is the right answer — e.g. if your goal is to, specifically, create runtime in Rust for GC-based language.

But it's impossible to say whether some “strategies” are best way to solve the issue. What that “strategy” even supposed to describe? What's the reason for it's existence?

The idea is not to unlearn JS, but to learn to compose programs in a tree, basically.

As I have repeated many times: Rust's story is story of structural programming all over again.

Structural programming won back 50-60 years ago but then the need to, somehow, do a GUI or very underpowered hardware gave people an excuse to go back to spaghetti code they love to make so much… only this time they are using callbacks instead of GoTos!

It's not impossible to create it in Rust, too (Rc/RefCell and Arc/Mutex can be used to create very convoluted structures, indeed and if everything else fails there are unsafe and pointers!), but the question is: why? What's the point?

It doesn't mean you should never use callbacks for anything: something externally-imposed task have requirements which make anything else unfeasible (e.g. if you are using existing library written in a different language), sometimes the task that you are trying to solve really requires the use of callbacks, but in Rust callbacks (like most data structures) have to be organized “mostly as a tree with select few cases where more complex graph is used explicitly”.

And most callbacks would be short-living one-liners, not something long-living.

So I looked at both futures and broadcast channels. Both appear to be very much rooted in the async world, which I was not trying to explore yet. (Up to this point, I was planning on having everything be synchronous and then slowly moving to asynchronicity. This is mostly a learning project for me.) That is not to say that I am not open to accepting async as a solution.

I have spent a little bit of time to try to apply what I understand from your idea to my situation, but I don't think I am doing it very well. I still am getting stuck with the closure/async block owning/borrowing/referencing the system.

The code:

mod player {
    type ID = u32;

    #[derive(Clone, Debug)]
    pub enum Event {
        PlayerJoins { id: ID },
        PlayerLeaves { id: ID },
    }
}

pub mod game {
    use super::player;
    use tokio::sync::broadcast;

    #[derive(Default)]
    struct Log(Vec<player::Event>);
    impl Log {
        async fn await_new_events(&mut self, rx: &mut broadcast::Receiver<player::Event>) {
            loop {
                let event: player::Event = match rx.recv().await {
                    Ok(event) => event,
                    _ => break,
                };
                self.new_event(event);
            }
        }
        fn new_event(&mut self, event: player::Event) {
            self.0.push(event)
        }
    }

    pub struct Game {
        player_events_tx: broadcast::Sender<player::Event>,
        player_events_rx: broadcast::Receiver<player::Event>,
        log: Log,
    }
    impl Game {
        pub async fn new() -> Self {
            let (tx, mut rx) = broadcast::channel(32);
            let mut log = Log::default();
            tokio::spawn(async {
                log.await_new_events(&mut rx);
            });

            Self {
                player_events_rx: rx,
                player_events_tx: tx,
                log,
            }
        }
        pub fn new_player_event(&self) {
            let _ = self
                .player_events_tx
                .send(player::Event::PlayerJoins { id: 1 })
                .unwrap();
        }
    }
}

Thanks for your thoughts, it made me think a lot! For me, it remains all very theoretical though. I tried to workout your suggestion in code, and I ended up with principally the same code as I wrote at the start of this topic. Is there a way to do the observer pattern in structural programming? If not, how would you organize this type of reactive* code? I notice I get stuck over and over again with ownership and communication.

*reactive using the previously mentioned definition of having many different "events" that the user could trigger.

This is my reworking of the code to see if I could make your suggestion work if I used traits instead, but I got no where.

mod player {
    type ID = u32;

    pub trait EventSubscriber {
        fn player_joins(&mut self, id: ID);
        fn player_leaves(&mut self, id: ID);
    }

    enum Event {
        PlayerJoins { id: ID },
        PlayerLeaves { id: ID },
    }
    
    struct Subject {
        subscribers: todo!() // TODO: What type should I use here?
    };

    #[derive(Default)]
    pub struct Log(Vec<Event>);
    impl EventSubscriber for Log {
        fn player_joins(&mut self, id: ID) {
            self.0.push(Event::PlayerJoins { id })
        }

        fn player_leaves(&mut self, id: ID) {
            self.0.push(Event::PlayerLeaves { id })
        }
    }
}

pub mod game {
    use super::player;

    pub struct Game {
        log: player::Log,
    }
    impl Game {
        pub fn new() -> Self {
            let mut log = player::Log::default();

            Self { log }
        }
    }
}

The async world and the world of futures are (as far as I know) synonymous, but channels are at least as useful for communicating between separate, blocking threads.

One of the worst approaches in Rust, unfortunately. You would immediately hit trouble because of functions color. There are some interesting ideas about how you can solve that in Rust 2024 but if you look on the calendar you'll see that we are still in 2022 (even if barely). Trying to switch from sync to async today is painful.

I can accept the idea to build sync program or async one, but to go from one to another? It's not impossible, obviously, but very painful.

Oh, sure. That's how you go from well-structured program to structured-programming-in-name-only.

ECS is one popular solution. Note that is wasn't even Rust invention — but since the problem it was trying to solve was similar solution ended up similar, too: instead of creating reactive code where everything reacts to everything and nothing is ever stable you are separating passive data from active actors.

While I generally agree this isn't a good match for most Rust programs, going back to a literal, direct answer for "what do I do to represent observers in Rust" is somewhat simple and the same as you would in JavaScript, a collection of functions:

struct Observer<T> {
  callbacks: Vec<Box<dyn FnMut(T)>>,
}

There's a trick or two to implementing subscribe, but more importantly this doesn't let you unsubscribe. You need to create identifiers if you want to unsubscribe later:

struct Observer<T> {
  callbacks: BTreeMap<u32, Box<dyn FnMut(T)>>,
  next_id: u32,
}

But you will quickly run into trouble when you try to return the unsubscribe handle: it can be called at any point, so it needs shared mutable access to the callbacks. This will kill this design pretty much dead as you chase down what that means.

All is not lost though: keep in mind the whole point of the Observer type as defined by rxjs and so on, is to represent a stream of events over time in a way that can be composed into a program as a whole. Asking how to add a callback so you can update state is kind of missing the point: they're not really intended to be used that way, and in fact you can tie up a JavaScript program into knots pretty badly if you mix and match "global" state and observables (I have plenty of times!).

What you need instead of this literal translation of the interface is just a way to represent this stream of values over time, and a way to combine them together. Fortunately, several people have taken stabs at this already, most prominently futures::stream - Rust

I'm not completely impressed with the current state of streams in Rust to be clear, it really needs a lot of fleshing out, both in documentation, spread of implementation, debugging, testing etc. I've plucked away at bits of this over time, but I'm still not sure what approach I want to take here when I'm not sure it's a good match for Rust in the first place. I'm thinking just starting with getting a marble testing crate out there would be good to at least make clear what it means to write a Stream combinator.

3 Likes

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.