Generic - Multiple impl block with different T bounds

I was playing around generic and it turns out, rust allows multiple impl with different trait bounds for T.
For example -

pub struct DataRepository<T> {
    storage: T
}
impl<T: InMemoryDataStore> DataRepository<T> {
}

impl<T: NetworkDataStore> DataRepository<T> {
}

But rust doesn't allow overriding, so following will fail -

impl<T: InMemoryDataStore> DataRepository<T> {
    pub fn get_data(&self) {
        self.storage.get("some_key".into());
    }
}

impl<T: NetworkDataStore> DataRepository<T> {
    pub fn get_data(&self) {
        self.storage.get_async("some_key".into());
    }
}

But I can, of course, declare a different method.
Playground

So why allow impl block with different trait bounds for T? What are the use cases?

It lets you enable different methods depending on what your type is parameterised by.

Some examples would be when you need to execute operations in a certain order and want to ensure this at compile time instead of runtime. This is an instance of the Typestate pattern.

You might also want to give a method a similar name, but switch between implementations. Like in actix_broker::Broker where you can use the type parameter to specify whether a message should be broadcast across the entire system, or just for the current thread.

You can see something similar in uom::si::Quantity where the base Quantity type is parameterised by the dimension (e.g. m/s² has a "1" for the length dimension and "-2" for the time dimension). There are lots of different combinations of quantities and conversion traits, so they decided to generate copies of things like ceil() and floor(). Given the way uom is designed, you would need higher-kinded types to express all this in a single generic method, so "overloading" with multiple impl blocks is an alternative.

Another occasion is where you may want to relax restrictions on what your type, T, needs to satisfy for certain methods. Have a look at this component that broadcasts the same message to multiple listeners and collects their responses.

// declaration and constructors.

pub struct Broadcast<M>
where
    M: Message + Send + 'static,
{
    listeners: Vec<Recipient<M>>,
}

impl<M> Broadcast<M>
where
    M: Message + Send + 'static,
{
    pub fn new() -> Self { Broadcast::with_listeners(Vec::new()) }
}

// Broadcasting a normal message by sending a copy to each listener.

impl<M> Broadcast<M>
where
    M: Message + Clone + Send + 'static,
{
    pub fn broadcast(
        &self,
        msg: M,
    ) -> impl Future<Output = Result<Vec<M::Result>, MailboxError>> {
        let pending: Vec<_> = self
            .listeners
            .read()
            .unwrap()
            .iter()
            .map(|l| l.send(msg.clone()))
            .collect();

        futures::future::try_join_all(pending)
    }
}

impl<M, E> Broadcast<M>
where
    M: Message<Result = Result<(), E>> + Clone + Send + 'static,
    M::Result: Send + 'static,
    E: Into<Error> + Send,
{
    /// A form of [`Broadcast::broadcast()`] which will flatten the result
    /// and short-circuit on failure, cancelling any messages which are still
    /// en-route.
    pub fn broadcast_fallible(
        &self,
        msg: M,
    ) -> impl Future<Output = Result<(), Error>> {
        let pending: Vec<_> = self
            .listeners
            .read()
            .unwrap()
            .iter()
            .map(|l| l.send(msg.clone()).map_result_into())
            .collect();

        futures::future::try_join_all(pending).map_ok(|_| ())
    }
}

I've created a specialised Broadcast::broadcast_fallible() so instead of getting a future which completes with Vec<Result<(), Error>> when all listeners respond, I get a single Result<(), Error> which will short-circuit and cancel any pending messages the moment anything responds with an error.

2 Likes

Cell<T> is an example of a standard library type with a lot of impls that have different bounds.

  • new, set, swap, replace, and into_inner work for any T that is Sized;
  • get and update only work when T: Copy;
  • as_ptr, get_mut and from_mut work even when T is not Sized;
  • take only works when T: Default.
5 Likes

Thanks guys for your generous help. It seems I still have a very long way ahead! :thought_balloon: