Stalemated between `impl` and `dyn` - vicious cycle

My wish is so simple - i just want a Hashmap of tower::Services - why is it so hard???

I am trying to build a Hashmap of tower::Service but am stuck trying to please the compiler

i started out with function that builds a tower service as below and i use impl as below which compiles ok:

fn build_svc<T>(_val: T) -> impl Service<String, Response=String, Error=BoxError> {
    let mut svc = ServiceBuilder::new();

    let svc = svc.layer(TimeoutLayer::new(Duration::from_secs(100)));
	// add layer specific to type T
    let svc = svc.service_fn( |req: String| async move { Ok::<_, BoxError>(req) });
    svc
}

because build_svc() is generic over T, i want to keep the return type "dynamic"
then i tried to define a struct that will hold the Hashmap of Services, as below (i had to fix compile errors - A & B):

type MyFuture = Pin<Box<Future<Output=Result<String, BoxError>> + Send + 'static>>;
type MyMap =HashMap<String, Box<dyn Service<String,Response=String, Error=BoxError, Future=MyFuture>>>; // compile ok
struct ServiceMap {
   // svcs: HashMap<String, impl Service<String>>, // compile error (A)
   // svcs: HashMap<String, Box<dyn Service<String,Response=_, Error=_, Future=_>>>, // compile error (B)
   svcs: MyMap,
}

Initially, i tried svcs: HashMap<String, impl Service<String>>, which gave me compile error (A), i.e. impl trait not allowed
So, i tried `svcs: HashMap<String, Box<dyn Service<String,Response=, Error=, Future=>>> in an effort to not constraint the associated types.
Nope, the compiler does not like this and insists i specify all the types, complaining that "
not allowed in type signatures"
I gave in and specified all the Associated Types (type MyMap above). Now the compiler is happy

Compile Error A
error[E0562]: `impl Trait` not allowed outside of function and method return types
--> src/tryimplvsdyn.rs:14:26
|
14 |    svcs: HashMap<String, impl Service<String>>,
|                          ^^^^^^^^^^^^^^^^^^^^
Compile Error B
--> src/tryimplvsdyn.rs:15:54
|
15 |    svcs: HashMap<String, dyn Service<String,Response=_, Error=_, Future=_>>, // compile error (A)
|                                                      ^        ^         ^ not allowed in type signatures
|                                                      |        |
|                                                      |        not allowed in type signatures
|                                                      not allowed in type signatures
|
help: use type parameters instead
|
13 | struct ServiceMap<T> {
    15 |    svcs: HashMap<String, dyn Service<String,Response=T, Error=T, Future=T>>,
    |

Great, compiler happy ...
Subsequently, i tried using the HashMap (struct ServiceMap) with fn build_svc() in the main code below:

#[tokio::main]
async fn main() {
    let svcs = MyMap::new();
    svcs.insert("test".to_string(), Box::new(build_svc())); // compile error (C)
}
Compile Error C
error[E0271]: type mismatch resolving `<impl Service<String> as Service<String>>::Future == Pin<Box<(dyn std::future::Future<Output = Result<String, Box<(dyn std::error::Error + Send + Sync + 'static)>>> + 'static)>>`
  --> src/tryimplvsdyn.rs:30:37
   |
8  | fn build_svc<T>(val: T) -> impl Service<String, Response=String, Error=BoxError> { // compiles error (D)
   |                            ----------------------------------------------------- the found opaque type
...
30 |     svcs.insert("test".to_string(), Box::new(build_svc(true))); // compile error (C)
   |                                     ^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `Pin`, found associated type
   |
   = note:       expected struct `Pin<Box<(dyn std::future::Future<Output = Result<String, Box<(dyn std::error::Error + Send + Sync + 'static)>>> + 'static)>>`
           found associated type `<impl Service<String> as Service<String>>::Future`
   = note: required for the cast to the object type `dyn Service<String, Error = Box<(dyn std::error::Error + Send + Sync + 'static)>, Future = Pin<Box<(dyn std::future::Future<Output = Result<String, Box<(dyn std::error::Error + Send + Sync + 'static)>>> + 'static)>>, Response = String>`
help: consider constraining the associated type `<impl Service<String> as Service<String>>::Future` to `Pin<Box<(dyn std::future::Future<Output = Result<String, Box<(dyn std::error::Error + Send + Sync + 'static)>>> + 'static)>>`
   |
8  | fn build_svc<T>(val: T) -> impl Service<String, Response=String, Error=BoxError, Future = Pin<Box<(dyn std::future::Future<Output = Result<String, Box<(dyn std::error::Error + Send + Sync + 'static)>>> + 'static)>>> { // compiles error (D)
   |                                                                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Again dear compiler is unhappy, now complaining "type mismatch" because the tower::Service return an opaque type?? Something about the Service::Future doesn't natch as it found an Associated type Service::Future instead of MyFuture??

So i tried changing the build_svc() to return Box<dyn Service> instead, again compiler insists i give all Associated types - which i obliged:

fn build_svc_b<T>(val: T) -> Box< dyn Service<String, Response=String, Error=BoxError, Future=MyFuture>> { 
    let mut svc = ServiceBuilder::new();

    let svc = svc.layer(TimeoutLayer::new(Duration::from_secs(100)));
    // add as layer a Service specific to generic type T
    let svc = svc.service_fn( |req: String| async move { Ok::<_, BoxError>(req) });
    Box::new(svc)
}

and still the compiler isn't happy ...

Compile Error D
error[E0271]: type mismatch resolving `<tower::timeout::Timeout<ServiceFn<[closure@src/tryimplvsdyn.rs:14:31: 14:82]>> as Service<String>>::Future == Pin<Box<(dyn std::future::Future<Output = Result<String, Box<(dyn std::error::Error + Send + Sync + 'static)>>> + 'static)>>`
 --> src/tryimplvsdyn.rs:9:32
  |
9 | fn build_svc(timeout: bool) -> impl Service<String, Response=String, Error=BoxError, Future=MyFuture> {
  |                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `Pin`, found struct `tower::timeout::future::ResponseFuture`
  |
  = note: expected struct `Pin<Box<(dyn std::future::Future<Output = Result<String, Box<(dyn std::error::Error + Send + Sync + 'static)>>> + 'static)>>`
             found struct `tower::timeout::future::ResponseFuture<_>`

At this point, i conclude there's no way to make the compiler happy! I can't have a HashMap with impl and i need to give all associated types if i use dyn which suddenly becomes not-dynamic anymore and signatures don't match. How can i fix this??
My wish is so simple - i just want a Hashmap of tower::Services - why is it so hard???

You definitely want some sort of dyn Trait and not impl Trait. It does not make sense to use impl Trait outside of arguments and return types of functions.

However, it seems that the Service trait is not really suitable to be boxed like this due to its Self::Future associated type. One trick you can use is to write a helper trait which hides the problematic associated type. For example, you can do this:

use futures::future::BoxFuture;

trait DynService<Request>: Send + Sync {
    type Response;
    type Error;

    fn ready<'a>(&'a mut self) -> BoxFuture<'a, Result<(), Self::Error>>;
    fn call(&mut self, req: Request) -> BoxFuture<'static, Result<Self::Response, Self::Error>>;
}

impl<T, R> DynService<R> for T
where
    T: tower::Service<R>,
    T: Send + Sync,
    T::Future: Send + 'static,
{
    type Response = <T as tower::Service<R>>::Response;
    type Error = <T as tower::Service<R>>::Error;
    
    fn ready<'a>(&'a mut self) -> BoxFuture<'a, Result<(), Self::Error>> {
        Box::pin(futures::future::poll_fn(move |cx| {
            self.poll_ready(cx)
        }))
    }
    fn call(&mut self, req: R) -> BoxFuture<'static, Result<Self::Response, Self::Error>> {
        let fut = tower::Service::call(self, req);
        Box::pin(fut)
    }
}

With this trait you should be able to box any service using the type

Box<dyn DynService<String, Response = String, Error=BoxError>>
2 Likes

Can i understand how or why the "Self::Future" associate type is an issue? What's the underlying Rust concepts coming into play?

The problem is that when using dyn Trait, you must specify the type for all associated types, but you can't and don't want to specify the type of Future here since it varies from one service to another.

yes, i believe that was the soul of my problem and i was stuck trying to find a way through. Pardon me, but in other languages .... this (Hashmap<String, ptr to Service>)
would have been a simple task. Think i have been meeting this trait (dyn / impl) etc and struggling with it for literally weeks (if not months)

Rust, so far, has been somehow seductively beautiful & enticing, yet somehow untouchable or unreachable to me ...

compiler errors can be cryptic...

Those other languages are making it simple by heap allocating everything. If you don't have multiple different future types, then you don't have this problem. Complexity is one of the costs of avoiding heap allocations.

1 Like

Wow! Wow! Wow! , my code compile after i use the DynService Traits !

1 Like

I have made similar experiences. To be honest, I gave up on Rust several times because it felt too complex for me, but eventually I tried again and each time I understood more. So I'd like to encourage you to not give up!

However, you have a point. Certain combinations of concepts can suddenly cause huge complexity issues that are very difficult to comprehend, even for experienced programmers. An example are combining asynchronous functions (using async) with traits. Or when you need generic associated types. Or combining iterators with async functions?

I can follow @alice that Rust avoids heap allocation in a lot of places and I really like that. Rust is a highly performant language and allows for abstract and generic programming. I do believe we have to pay a certain price here when it comes to complexity, but I also believe that some things could be improved.

Correct me if I'm wrong, but I assume Rust wasn't designed from the beginning to keep asynchronous programming in mind? I sometimes feel like noticing that a lot since I got into asynchronous programming with Rust. Most things work fine, except when certain combinations don't.

Anyway, I'd still encourage you to not give up. This forum helped me a lot to get a deeper understanding for the language, and I hope some of the missing bits and pieces to make asynchronous Rust work smoother will come together eventually. So far, I'm amazed by Rust (including its asynchronous parts), and Rust brings together some things that no other language is capable of. So it's easy to say that other languages are easier. Their price becomes due at runtime.

1 Like

Thanks for the encouragement - i do get frustrated and desperate - especially since i'm the only user of Rust in my company currently and i am building something with a deadline (slipping though thanks to rust).

Going to cost me a lot of time if i decide to bail on rust and go back to something more familiar.

But i do wish there's a really solid book that covers Rust from beginner through intermediate and adanced concepts - a really good Rust Bible and another for Tower

This is hitting the nail on the head. To solve the problem in this thread, you have to know about:

  1. The difference between impl Trait and dyn Trait
  2. Associated types and how they interact with trait objects (e.g. you must specify them)
  3. Poll methods such as poll_ready.
  4. Futures and the fact that there are many different future types.
  5. Blanket impls of custom traits.

Even if you know about all the features, the idea of writing an extra helper trait is not obvious.

This is also one of the problems with writing books or tutorials - writing about all the features is not impossible, but you also have to talk about all the ways to combine them, and there are many more ways to combine features than there are features.

4 Likes

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.