Please help me break my pattern, I'm coming from PHP!

Hello,

As a PHP developer, I came across Rust and really love it for all it's features. In my spare time I'd like to work on some personal projects, just to learn Rust better. However, I keep running into the same patterns I use with PHP, which are impossible to implement in Rust (by definition).

I need someone who can tell me how to break those patterns so that I can implement that in a memory-safe way, as Rust intends.

The pattern that I'd like to break can be described the best by using the Symfony (PHP framework) "event-dispatcher" example. And please, I'm not looking for libraries that can do event dispatching or replace my code with another library, I want to break the pattern (which is stuck in my brain) that is used in this particular event-dispatcher:

The scenario:

  • You can add event "listeners" by registering callbacks for a specific event name
  • You can dispatch a specific event object using the generic event-dispatcher
  • The registered callbacks will receive the specific event object

The problem:

  • A specific object is dispatched and received in a callback through a generic event-dispatcher

In PHP, this could be something like this:

class MySpecificEvent {}
class SomeOtherSpecificEvent {}

class EventDispatcher {
    private $listeners = [];

    public function addListener(string $name, callable $callback): void {
        $this->listeners[$name][] = $callback;
    }

    public function dispatch(string $name, object $event): void {
        foreach ($this->listeners[$name] as $callback) {
            $callback($event);
        }
    }
}

And the usage:

$eventDispatcher = new EventDispatcher();
$eventDispatcher->addListener("my-specific-event", function (MySpecificEvent $event) {});
$eventDispatcher->addListener("some-other-specific-event", function (SomeOtherSpecificEvent $event) {});

$event = new MySpecificEvent();
$eventDispatcher->dispatch("my-specific-event", $event)

$event = new SomeOtherSpecificEvent();
$eventDispatcher->dispatch("some-other-specific-event", $event)

This means that:

  1. The signature of addListener allows an callback with an arbitrary object as argument to be passed
  2. The signature of dispatch allows an arbitrary object to be passed as 2nd argument
  3. The callback would receive the arbitrary object, which would succeed or throw an exception depending on whether the object is of the correct type

Since Rust is memory-safe, it is not possible to just pass arbitrary objects as arguments, they have to be typed and the functions must have a fixed signature.

This "simple" example from above shows a patterns that is stuck in my brain... it might be poisoned by all these years of PHP.

How can such logic be transformed to memory/type-safe Rust? I want to be able to make the mapping in my head, so that I can translate similar scenarios from PHP to Rust more easily.

Any help would be greatly appreciated!

Thanks,
Raymond

2 Likes

I would probably do something like this:

use std::mem::*;
use std::ops::Deref;
use std::collections::HashMap;

#[non_exhaustive]
pub enum Event {
    Event1,
    Event2 ( usize ),
}

pub trait EventListener {
    fn process_event(&self, event: &Event );
    fn get_desired_events(&self)->Vec<Discriminant<Event>>;
}

impl<EL:EventListener, Ptr: Deref<Target=EL>> EventListener for Ptr {
    fn process_event(&self, ev: &Event ) {
        self.deref().process_event(ev);
    }
    fn get_desired_events(&self)->Vec<Discriminant<Event>> {
        self.deref().get_desired_events()
    }
}

#[derive(Default)]
pub struct Dispatcher {
    listeners: Vec<Box<dyn EventListener>>,
    dispatch_table: HashMap<Discriminant<Event>, Vec<usize>>,
}

impl Dispatcher {
    pub fn add_listener(&mut self, listen: impl EventListener + 'static) {
        let id = self.listeners.len();
        let events = listen.get_desired_events();
        self.listeners.push(Box::new(listen));
        for ev in events {
            self.dispatch_table.entry(ev).or_default().push(id);
        }
    }
    
    pub fn dispatch(&self, ev:Event) {
        if let Some(ref interested) = self.dispatch_table.get(&discriminant(&ev)) {
            for id in interested.iter().copied() {
                self.listeners[id].process_event(&ev);
            }
        }
    }
}

A few design notes:

  • #[non_exhaustive] ensures that listeners aren't broken when new possible events get added.
  • Using enum variants with data ensures that each message comes with the necessary information
  • get_desired_events() is optional, but it allows the dispatcher to skip listeners that aren't interested in some of the possible events.
  • Dispatcher takes ownership of the EventListener, but the impl<...> EventListener for Ptr means that any reference or smart pointer to an event listener can also be used as an event listener.
1 Like

Can you add some usage code, so that I can play with it to see how it actually works?

Sure; here's a playground link with some examples to get you started: Rust Playground

4 Likes

Thank you very much, that will keep me busy for a while to fully understand what's happening, but that would be worth it!

If you want a more detailed explanation of any part of this, feel free to ask; it's hard for me to know up front what's going to be easy or hard to understand.

@2e71828, I've stripped your example from unrelated logic (RC, Deref, etc.) and extended it with EventListener and even used some actual contextual events: Rust Playground

I see and understand what you did and I did not know about Discriminant to uniquely identify an Enum variant!

The Enum does solve the "generic" issue because the listener/subscriber uses match to target the correct event, however it also now requires to initialize the event with a dummy value when passing this event to listen/subscribe on.

Is there any way to get rid of that dummy value?

Thanks for your help so far, I begin to see a pattern that might help me!
Raymond

1 Like

As far as I know, there’s no way to produce a discrimnant without already having a fully-constructed example. Looking at your code, an interesting avenue to explore might be to subscribe new listeners to all events, and then put an unsubscribe call ih the default match arm in place of unimplemented— at that point, you have an event to take the discriminant of.

It’s slightly less efficient because you’re sending some events to objects that don’t care about them, but there’s also less risk of getting a mismatch between the events an object is subscribed for and those it knows how to handle.

@2e71828, I also forgot to mention that with this solution it won't be possible to have this as library, where the user can define events in his own application. Since the Enum must be known in the library, and the user can't extend it.

For that, you could have something like a UserEvent(String, Box<dyn Any>) variant, but it does start to get complicated.

Actually, thinking about it some more, you could use a newtype pattern where each event has a distinct type, and use an any::TypeId as the key for your dispatch table.

The handlers would receive a Box<dyn Any>, and would use downcast to get back the original type with all its data.

@2e71828, can you show an example with Any? It doesn't have to be a fully working example, just to support what you mean. Usually when I add boxing and traits, I get different issues, so I'd like to see what you mean.

As I was writing the example, I instinctively reached for closures instead of defining my own trait. It makes the code more concise, but maybe a little harder to follow. Overall, I think this probably ended up better than my first try— I still don’t have a great instinct for when to choose enums vs. dynamic dispatch.

use std::any::*;
use std::default::Default;

struct Listener {
    event: TypeId,
    handler: Box<dyn Fn(&dyn Any)>,
}

#[derive(Default)]
struct Dispatcher {
    subs: Vec<Listener>,
}

impl Dispatcher {
    pub fn add_listener<Event: 'static>(&mut self, action: impl Fn(&Event) + 'static) {
        self.subs.push(Listener {
            event: TypeId::of::<Event>(),
            handler: Box::new(move |ev: &dyn Any| {
                (action)(ev.downcast_ref().expect("Wrong Event!"))
            }),
        });
    }

    pub fn dispatch<Event: 'static>(&self, ev: &Event) {
        for l in self.subs.iter() {
            if TypeId::of::<Event>() == l.event {
                (l.handler)(ev);
            }
        }
    }
}



fn main() {
   let mut dispatcher:Dispatcher=Default::default();
   
   struct StringEvent(&'static str);
   struct PointEvent { x:f32, y:f32 }

   dispatcher.add_listener(|ev:&StringEvent| println!("StringEvent: {}", ev.0));
   
   
   dispatcher.dispatch(&StringEvent("hello"));
   dispatcher.dispatch(&PointEvent { x:0.0, y:1.0 });
}

(Playground)

@2e71828, thank you for showing me the Any trait, by binding the type to the add_listener function and wrapping the actual closure with another to handle the different closure argument types this works nicely in my example about event listeners/subscribers.

I again took the liberty to rewrite your example to match my previous example and I failed in adding mutable events where the closures are able to modify the state of the event, which could be useful for hypothetical event bubbling.

use std::any::{Any, TypeId};

fn main() {
    let mut dispatcher = Dispatcher::new();

    dispatcher.add_listener(|event: &BootstrapEvent| println!("Application bootstrapped"));
    dispatcher.add_listener(|event: &UserRegisteredEvent| println!("User '{}' registered", event.user.username));
    dispatcher.add_listener(|event: &ProductRemovedEvent| println!("Product '{}' (#{}) removed", event.product.name, event.product.code));
    dispatcher.add_listener(|event: &ControllerResponseEvent| {
        println!("Response: {}", event.response.content);
        event.response.content = "Test"; // `event` is a `&` reference, so the data it refers to cannot be written
    });
    dispatcher.add_listener(|event: &ControllerResponseEvent| println!("Response: {}", event.response.content));

    dispatcher.dispatch(&BootstrapEvent {});
    dispatcher.dispatch(&UserRegisteredEvent { user: User { username: "raymond" } });
    dispatcher.dispatch(&ProductRemovedEvent { product: Product { name: "Dinner Table", code: 52241 } });
    dispatcher.dispatch(&ControllerResponseEvent { response: Response { content: "" } });
}

struct Listener {
    event: TypeId,
    handler: Box<dyn Fn(&dyn Any)>,
}

struct Dispatcher {
    listeners: Vec<Listener>,
}

impl Dispatcher {
    pub fn new() -> Self {
        Self {
            listeners: Vec::new(),
        }
    }

    pub fn add_listener<Event: 'static>(&mut self, action: impl Fn(&Event) + 'static) {
        self.listeners.push(Listener {
            event: TypeId::of::<Event>(),
            handler: Box::new(move |event: &dyn Any| {
                (action)(event.downcast_ref().expect("Wrong Event!"))
            }),
        });
    }

    pub fn dispatch<Event: 'static>(&self, event: &Event) {
        for listener in self.listeners.iter() {
            if TypeId::of::<Event>() == listener.event {
                (listener.handler)(event);
            }
        }
    }
}

struct BootstrapEvent;

struct UserRegisteredEvent {
    pub user: User,
}

struct ProductRemovedEvent {
    pub product: Product,
}

struct ControllerResponseEvent {
    pub response: Response,
}

pub struct User {
    pub username: &'static str,
}

pub struct Product {
    pub name: &'static str,
    pub code: u16,
}

pub struct Response {
    pub content: &'static str,
}

I played with the mutable modifiers, but failed, so I revered it all, keeping my scenario in there, with the error message as well. How can this example be modified to make the closures capable of modifying the events?

Also, I don want to sound unthankful, because your examples actually did help me a lot in understanding alternative solutions for the patterns that I use in PHP, but your last solution using closures is actually more a solution for the event listener/subscriber use-case I brought to table, which was an hypothetical scenario, not an actual problem I try to solve.

As I was writing the example, I instinctively reached for closures instead of defining my own trait.

I still think that if you have an example using a trait will be more closely forming a solution for my PHP to Rust pattern translation. May I ask you again to show such an example as well?

Like I said, I'm really thankful that you already helped me this far, and the Enum and closure method actually are translatable for a couple of scenarios, but I think the Trait method would be the actual thing I'm searching for, as it would be more close to the implementation in PHP, but I don't event know if that would be possible, as I mentioned in my initial post.

Regards,
Raymond

If you want to modify the events themselves, all of the &Event and &dyn Any references need to change to &mut, and event.downcast_ref will change to event.downcast_mut. If you want the event handlers to have their own internal state they can mutate, you’ll need to change Fn to FnMut, and add various mut declarations wherever the compiler complains.

As far as doing this with traits, it’s not much different than with closures:

|x:String| x.len()

is a convenient shorthand for constructing an object that implements the Fn(String)->usize trait, which is roughly defined this way:

pub trait Fn {
    fn call(&self, x:String)->usize;
}

They can be freely replaced in the example with structs that implement your own custom trait with whatever set of methods you need, as long as the trait is object safe. In particular, you probably want to have a trait method that accepts a &dyn Any or &mut dyn Any, and use the turbofish operator to specify explicitly which event type you’re considering:

if let Some(ev1) = ev.downcast_ref::<EventType1>() {
   // ev is a dyn Any reference to an EventType1 value
   // ev1 is of type &EventType1 here
} else {
   // ev is a reference to some other concrete type
}

I primarily used closures because the example only needed one function in the trait, and I wanted to minimize the amount of precision typing because I don’t have a good keyboard on the weekends.

@2e71828, I really want to thank you again for your patience and your excellent style of explaining! I missed one &mut dyn Any while trying to make the events mutable, I fixed that and it works as an alternative to the Enum method.

Based on your explanation I was also able to rewrite the closure logic into a method that excepts traits, so that you can add structs that implement EventListener to add_listener and that they are bound and boxed the same way as the closures were to keep the context of the event stored. However after building it I came to the conclusion it had the same drawbacks as the Enum method: I have to add a dummy initialization to the events when registering the listeners.

But that doesn't matter right now, I now have learned about Discriminant and Any and I'm on my way of actually seeing possible alternatives to the PHP way of passing specific objects into methods based on a generic signature. I have to keep playing with this and keep adding these kind of examples to my knowledge base and continue my learning...

Thanks you very much!
Raymond

1 Like

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.