Are trait objects the right choice for my bot command system?

For context, I’m re-implementing a Telegram bot I wrote in Java a long time ago. A command in that project is a subclass of the abstract class Command, and all available commands have an instance in a HashMap<String, Command>; on incoming messages, the handler checks if the first word in the message matches a key, and if so, the Command object processes the request, returns a result and optionally also processes the result from telegram for some simple caching (that cache is also saved on and, at startup, loaded from disk).

As this is using classic OO polymorphism, the first thought when re-implementing it in Rust would be to define a Cmd trait and then use a HashMap<&str, Box<dyn Cmd>> to store the actual commands:

pub trait Cmd {
    fn name(&self) -> &'static str;
    fn usage(&self) -> &'static str;
    fn process(&self, context: CmdContext, state: Option<&CmdState>) -> CmdResult;
    fn process_result(&self, context: CmdContext, state: Option<&CmdState>) -> Option<CmdState>;
}

(The code is just the basic idea, but I think you should get it.)
This way, I would get the same polymorphism benefits I have in Java in the same way (with even more freedom because the implementing type can be whatever I want), but I know there is a runtime cost for using trait objects like that, and my main reason for wanting to move away from Java is performance and memory usage (closely followed by the fact that I don’t like the language anymore).

So the question is, is this the right way to do it for my use case?
Are there good alternatives? I could imagine defining a struct which has two Fns as fields, so every command would have the same type, but I don’t know if that would be as flexible.

1 Like

There is a runtime cost for trait objects, since they dynamically dispatch to the correct method at runtime. However, since this is exactly what you want in this case, this runtime cost isn’t actually an overhead – you need to pay it in any case, regardless of how you implement this dynamic dispatch. And having a single dynamic dispatch for each incoming Telegram message doesn’t sound like something that could become a performance bottleneck anyway.

If you encounter performance problems, the only way to fix them is to profile your code and figure out the cause for the performance bottleneck. My gut feeling is that a Telegram bot is unlikely to be CPU-bound, so it’s unclear whether reimplementing in Rust will actually solve your problem.

2 Likes

If you know all of your commands upfront, you can use enums instead of trait objects to get better performance.

Rust enums are way more powerful than Java enums, so give them a look.

2 Likes

Fn is itself a trait, so you’d end up needing to box it instead – then you’d have two allocations with vtables instead of one.

Having one dynamic dispatch is perfectly appropriate here. As is the goal with futures, you have one indirection at the outside, which then goes to optimized statically-dispatched internals.

3 Likes

Thanks to all for the input!

@smarnach : After thinking about it for some time, I have to admit that the main reason is being fed up with Java and wanting to move to a different language, and since I like Rust a lot, I decided to port it to that. There isn’t any particular performance problem involved here. This thread is more asking for advice and if my approach is right or could be done better in some way.

@KrishnaSannasi : Oh, I know very well how powerful they are; however, since many of my commands have shared behaviour, my preference goes towards defining a type per command kind; implementing an enum was actually my first idea at this, but then I realized that I can’t impl an enum variant, so it would have to be a giant match and I didn’t really want that.

@scottmcm : Didn’t realize that Fn was a trait, so that settles it. Thanks!

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.