Impl a struct only if it implements another trait

Hi I want to implement a struct only if RetryItem implements Retry trait.
How can i do it?

use reqwest::Client;
use futures::Future;
use std::hash::Hash;
use futures::stream::FuturesUnordered;
use futures::StreamExt;
use std::collections::HashMap;

struct RetryItem<T, Z> {
    session: Client,
    key: Z,
    data: T,
}

struct RetrySystem<T, Z, Y>
where
    Z: Future<Output = (RetryItem<T, Y>, Result<(), Box<dyn std::error::Error>>)>,
    Y: Eq + Hash,
    T: Retry<T, Y>,
{
    queue: Vec<T>,
    stream: FuturesUnordered<Z>,
    concurency: u64,
    retry_count: HashMap<Y, u64>,
    max_retries: u64,
}

trait Retry<T, Y>
where
    Y: Eq + Hash,
{
    async fn process(item: RetryItem<T, Y>) -> (RetryItem<T, Y>, Result<(), Box<dyn std::error::Error>>);
}

impl<T, Z, Y> RetrySystem<T, Z, Y>
where
    Y: Eq + Hash,
    T: Retry<T, Y>,
    Z: Future<Output = (RetryItem<T, Y>, Result<(), Box<dyn std::error::Error>>)>,
{
    async fn next(&mut self) {
        while let Some((item, result)) = self.stream.next().await {
            if let Ok(result) = result {
                // To do
            } else {
                let entry = self.retry_count.entry(item.key);
                let entry_value = entry.or_insert(0);

                if *entry_value <= self.max_retries {
                    self.stream.push(Retry::<T, Y>::process(item));
                }
            }
        }
    }
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
  --> src/lib.rs:49:38
   |
31 |     async fn process(item: RetryItem<T, Y>) -> (RetryItem<T, Y>, Result<(), Box<dyn std::error::Error>>);
   |     ----------------------------------------------------------------------------------------------------- `Retry::process` defined here
...
49 |                     self.stream.push(Retry::<T, Y>::process(item));
   |                                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot call associated function of trait

For more information about this error, try `rustc --explain E0790`.
error: could not compile `playground` (lib) due to 1 previous error

You can put arbitrary types on the left side of a where clause, and not just type parameters:

where RetryItem<T,Y>: Retry<T,Y>

Could you provide a example using my code? I've tried adding it to my where clause to impl RetrySystem but it results in same error. But i don't understand what do you mean by "left side of where clause".

You're trying to call an associated function on a trait. You need to actually implement the trait for the type and call the associated function using <MyType as Retry<...>>::process(item).

I was trying to implement RetrySystem so i could reuse it for different types just by implementing Retry trait for it. Is there any way to accomplish that ?

I don't know which type is meant to implement Retry in your code, but whichever it is you should call Retry::process with the syntax I mentioned above.

Edit - I see nowthere's a Retry bound on T, so you should callit using T::process(...)

Ok thanks now i can call it. But compiler is still not happy for some reason.

use reqwest::Client;
use futures::Future;
use std::hash::Hash;
use futures::stream::FuturesUnordered;
use futures::StreamExt;
use std::collections::HashMap;

struct RetryItem<T, Z> {
    session: Client,
    key: Z,
    data: T,
}

struct RetrySystem<T, Z, Y>
where
    Z: Future<Output = (RetryItem<T, Y>, Result<(), Box<dyn std::error::Error>>)>,
    Y: Eq + Hash + Copy,
    T: Retry<T, Y>,
{
    queue: Vec<T>,
    stream: FuturesUnordered<Z>,
    concurency: u64,
    retry_count: HashMap<Y, u64>,
    max_retries: u64,
}

trait Retry<T, Y>
where
    Y: Eq + Hash + Copy,
{
    async fn process(item: &RetryItem<T, Y>) -> (RetryItem<T, Y>, Result<(), Box<dyn std::error::Error>>);
}

impl<T, Z, Y> RetrySystem<T, Z, Y>
where
    Y: Eq + Hash + Copy,
    T: Retry<T, Y>,
    Z: Future<Output = (RetryItem<T, Y>, Result<(), Box<dyn std::error::Error>>)>,
    RetryItem<T,Y>: Retry<T,Y>
    
{
    async fn next(&mut self) {
        while let Some((item, result)) = self.stream.next().await {
            if let Ok(result) = result {
                // To do
            } else {
                let entry = self.retry_count.entry(item.key);
                let entry_value = entry.or_insert(0);

                if *entry_value <= self.max_retries {
                    let result = T::process(&item);
                    self.stream.push(result);
                }
            }
        }
    }
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0308]: mismatched types
   --> src/lib.rs:52:38
    |
34  | impl<T, Z, Y> RetrySystem<T, Z, Y>
    |         - expected this type parameter
...
52  |                     self.stream.push(result);
    |                                 ---- ^^^^^^ expected type parameter `Z`, found associated type
    |                                 |
    |                                 arguments to this method are incorrect
    |
    = note: expected type parameter `Z`
              found associated type `impl futures::Future<Output = (RetryItem<T, Y>, Result<(), Box<(dyn std::error::Error + 'static)>>)>`
note: method defined here
   --> /playground/.cargo/registry/src/index.crates.io-6f17d22bba15001f/futures-util-0.3.30/src/stream/futures_unordered/mod.rs:166:12
    |
166 |     pub fn push(&self, future: Fut) {
    |            ^^^^

For more information about this error, try `rustc --explain E0308`.
error: could not compile `playground` (lib) due to 1 previous error

(I'm only replying to this comment, and may have missed something earlier in the thread. Sorry if I say something redundant or irrelevant.)

The immediate cause of the error is that calls to Retry::process return an opaque type that implements Future<Output = ...>, but you need a specific type that implements Future in order to push the result into self.stream. Namely, given the type parameters, you need a Z.

Or to phrase it more generally, ignoring the "opaque" aspect: every implementor of Reply<T, Y> can choose a different Future type to return from process, and you need to restrict the implementations to those that return Z.[1]

Here's a way to be able to state that restriction:

+// The change here is to make the return type nameable...
+// ...but see also the rest of this forum post
 trait Retry<T, Y>
 where
     Y: Eq + Hash + Copy,
 {
-    async fn process(item: &RetryItem<T, Y>) -> (RetryItem<T, Y>, Result<(), Box<dyn std::error::Error>>);
+    type Fut: Future<Output = (RetryItem<T, Y>, Result<(), Box<dyn std::error::Error>>)>;
+    fn process(item: &RetryItem<T, Y>) -> Self::Fut;
 }

+// The change here makes the return type be `Z`
 impl<T, Z, Y> RetrySystem<T, Z, Y>
 where
     Y: Eq + Hash + Copy,
-    T: Retry<T, Y>,
+    T: Retry<T, Y, Fut = Z>,
     Z: Future<Output = (RetryItem<T, Y>, Result<(), Box<dyn std::error::Error>>)>,
     RetryItem<T,Y>: Retry<T,Y>

However, this also introduces another semantic change which may not be desirable. Before the changes, the output of process captured the input lifetime on the &RetryItem<T, Y>. So it wasn't really a single type, it was a type constructor -- you got a different type for every input lifetime. It was more like this:

trait Retry<T, Y>
where
    Y: Eq + Hash + Copy,
{
    type Fut<'a>: Future<Output = (RetryItem<T, Y>, Result<(), Box<dyn std::error::Error>>)>
    where
        T: 'a,
        Y: 'a
    ;
    fn process(item: &RetryItem<T, Y>) -> Self::Fut<'_>;
}

With the version in the playground, implementors of Retry<T, Y> are no longer allowed to capture the input lifetime, e.g. by capturing item in the future. They'll have to extract what they need from *item before an async block, or something like that. There's a good chance this is too restrictive.

On the other hand, if implementors are allowed to capture the lifetime, there's no way to unify the output types (which differ by lifetime) with the type parameter Z (which represents a single type -- it can't represent a bunch of types that differ by lifetime).

On top of that, I'm not seeing a way forward if the playground is too restrictive, offhand. If the process return type captures a lifetime, that lifetime can't outlive the while loop in the next method, so there's no way for it to be long enough to be part of the implementing RetrySystem (which has to be valid for longer than the entirety of the function body). Some larger redesign would be necessary.


  1. It's actually a little worse than that, which we'll return to shortly. ↩ī¸Ž

3 Likes

Addendum: Taking another step back, most future types are compiler generated, not specific types, so the playground above is probably a non-starter even ignoring the lifetime issues. That is, you probably needed to type-erase the futures anyway (Pin<Box<dyn Future(Output = ..) + '_>>), instead of having a specific future type as a parameter on RetrySystem.

But the lifetime issues discussed at the end of my previous comment will be there even if you type-erase the futures.

1 Like

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.