Expected type parameter `S`, found struct

Hello,

I am trying to implement a builder of services where all the services implement the Service trait.

pub trait Service {
	fn run(self);
}

pub trait Builder<S: Service> {
    fn build(self) -> Result<Vec<S>, bool>;
}

pub struct HttpService;
impl Service for HttpService {
    fn run(self) {}
}

pub struct WsService;
impl Service for WsService {
    fn run(self) {}
}

pub struct MyBuilder;
impl<S: Service> Builder<S> for MyBuilder {
    fn build(self) -> Result<Vec<S>, bool> {
        let mut v: Vec<S> = vec![];
        v.push(HttpService {});
        v.push(WsService {});
        
        Ok(v)
    }
}

When building, i am getting an error:

expected type parameter `S`, found `HttpService`
  • Can you shed some light on why this approach doesn't work?
  • How would this pattern be alternatively implemented?
  1. Always bear in mind that the caller chooses S, not the callee. So if I, the caller, want to call <MyBuilder as Builder<MyService>>::build(), I wouldn't get a Vec<MyService>, I would get a vector with a HttpService and a WsService in it. The callee (MyBuilder::build) can't decide what S is supposed to be in its function body.
  2. Having established that your callee can't choose S, you might be tempted to think that return position impl Trait (RPIT) would be the way to go, as this would allow us to return an opaque type that implements Service from our method, without the caller being allowed to choose what type we actually return. Unfortunately that won't work in your case either, as you want to return two different concrete types (HttpService and WsService) from your method and RPITs only allow you to return one concrete type.
  3. Which brings us to trait objects. Given that Service is object safe in your example, we can create trait objects from our HttpService and WsService instances. Same as RPITs, they avoid the generic parameter that lets the caller choose what we want to return. But unlike RPITs, they allow us to coerce multiple concrete types implementing Service into a different concrete type dyn Service, allowing us to return them from our build method.

Here an example that compiles by using trait objects:

pub trait Service {
	fn run(self);
}

pub trait Builder {
    fn build(self) -> Result<Vec<Box<dyn Service>>, bool>;
}

pub struct HttpService;
impl Service for HttpService {
    fn run(self) {}
}

pub struct WsService;
impl Service for WsService {
    fn run(self) {}
}

pub struct MyBuilder;
impl Builder for MyBuilder {
    fn build(self) -> Result<Vec<Box<dyn Service>>, bool> {
        let mut v = vec![];
        v.push(Box::new(HttpService {}) as _);
        v.push(Box::new(WsService {}) as _);
        
        Ok(v)
    }
}

Playground.

7 Likes

@jofas Thank you very much for this very clear explanation.

I am curious though, if this is possible without boxing or not... how would you approach this? I have been checking out Rust's design patterns and noticed this which is quite similar to what you just described.

Thanks again!

Your playground link in your OP doesn't point to your code (click Share in the upper right). Which is fine, I just copied your code block, just letting you know for next time.


To add on to what @jofas said in point (1), there's an additional problem in your OP to the caller choosing the paramter S. And that is, Vec is a homogeneous collection -- it only supports one type, and you're trying to put multiple types into it. Or depending on your background, this could perhaps just be phrased: Rust is strongly and statically typed.

If you want to store both types of services in that Vec, you need a way to get them into the same type. Type erasure (dyn Trait) is one such approach. It requires some sort of indirection (like a Box).

If you know all the implementing types you want to support ahead of time, an enum is an alternative to dyn Trait, and doesn't require the indirection.

3 Likes

The enum_dispatch crate is a great way to avoid boxing and still use multiple structs that implement a trait. Plus the performance of calls to the trait methods is improved. Check out the crate doc.

2 Likes