Generic or Trait Object

Hi,
I'm writing a web API with warp and, coming from a Java background, I have multiple "services" (structs that impl Traits) which I want to combine.

I got it running using generics but the code is somehow now that readable
#[async_trait]
pub trait CalendarService: Send + Sync {
    async fn insert() -> Option<Calendar>;
}

pub struct CalendarSVC<Repo: CalendarRepository> {
    repo: Arc<Repo>,
}

impl<Repo: CalendarRepository> CalendarSVC<Repo> {
    pub fn new(repo: Arc<Repo>) -> Self {
        CalendarSVC { repo }
    }
}

#[async_trait]
impl<Repo: CalendarRepository> CalendarService for CalendarSVC<Repo> {
    async fn insert() -> Option<Calendar> {
        None
    }
}

In this simple case I only needed one parameter, I wonder how it might look if I needed multiple such "services"

on the other hand I could use a trait object
#[async_trait]
pub trait CalendarService: Send + Sync {
    async fn insert(&self) -> Option<Calendar>;
}

pub struct CalendarSVC {
    repo: Arc<dyn CalendarRepository>,
}

impl CalendarSVC {
    pub fn new(repo: Arc<dyn CalendarRepository>) -> Self {
        CalendarSVC { repo }
    }
}

#[async_trait]
impl CalendarService for CalendarSVC {
    async fn insert(&self) -> Option<Calendar> {
        None
    }
}

That looks way cleaner and more concise.
I remember I did this because of performance implications that I do not remember anymore. That said I don't know if that perfomance gain could be worth the extra hassle.

Can you enlighten me what gains there may be? All this is for learning purposes and might never see the light of day or might only be used by very few people.

One other aspect is: it is very likely all those services will only have static functions and there is no need to use &self in funtion calls other than for accessing other services. That would not be needed If I used generics.

There are tradeoffs to be made. What would be the more rusty way?

Thanks for your time :slight_smile:

As far as I'm aware, the tradeoffs would be:

  • generics:
    • + Somewhat more efficient (or at least unlock more optimizations opportunities for the compiler), especially for small functions called very often.
    • + Very good for storing data (like Vec), because you avoid some indirection. In your case there is already an Arc, so this does not apply.
    • + Can still store dyn Trait objects in your example:
      pub struct CalendarSVC<Repo: CalendarRepository + ?Sized> {
          repo: Arc<Repo>,
      }
      
      let repo: Arc<dyn Repo> = /* ... */;
      let calendar_svc = CalendarSVC { repo }; // works exactly like the dyn example
      
    • - Risk of bloating code if the generic structure is instantiated with a lot of different types.
    • - As you pointed out, it might be harder to read (especially with a lot of types parameters).
  • dyn Trait:
    • + Useful to store different types. In your example, this might allow you to create a Vec<CalendarSVC> where the underlying Repo type is different.
    • + If the trait's functions are not called that often (like IDK, less than once every 1/100 sec ? Probably even less than that) or have little chance of getting inlined, the performance blow will probably be very small. I believe modern computers are pretty friendly to the kind of indirection that dyn uses.
    • - As seen above, this is strictly less powerful than generics in your example.

Huh, hope this helps you and doesn't confuse things :sweat_smile: