Newbie: Understanding traits and associated types

Hello, I'm very new to Rust and have just been playing around with different things.

Most of the problems I've encountered were solved by searching, but I've spent a lot of time on this one with no luck so far. I admit that it could very well be due to sorely lacking understanding about fundamentals, which is something I'm hoping to amend with this post.

I've created a playground with a reproduction of the issue. The Handler trait actually is from the lambda_runtime Handler trait, not something I've created myself.

In trying to implement the Handler trait, I'm blocked by the associated type Fut. From what I understand so far, Future is a trait not a type, so it can't be used to specify the associated type in the implementation.

So, my question is, how should Handler be implemented? And why is what I've been trying not working?

I would really appreciate your help!

use async_trait::async_trait;
use anyhow::Error;
use core::future::Future;

trait Handler<A, B> {
    type Error;
    type Fut: Future<Output = Result<B, Self::Error>>;
    fn call(&self, request: A) -> Self::Fut;
}

struct WorkerRequest {
    id: String,
}

struct WorkerResult {
    message: String,
}

#[async_trait]
trait Worker {
    async fn do_work(&self, request: WorkerRequest) -> Result<WorkerResult, Error>;
}

struct MyHandler<T: Worker> {
    worker: T,
}

impl<T: Worker> MyHandler<T> {
    fn new(worker: T) -> Self {
        MyHandler {
            worker: worker,
        }
    }
    
    async fn work(&self, request: WorkerRequest) -> Result<WorkerResult, Error> {
        let result = self.worker.do_work(request).await?;
        Ok(result)
    }
}

impl<T, F> Handler<WorkerRequest, WorkerResult> for MyHandler<T>
where
    T: Worker,
    F: Future<Output = Result<WorkerResult, Error>>
{
    type Error = Error;
    type Fut = F;

    fn call(&self, request: WorkerRequest) -> Self::Fut {
        self.work(request)
    }
}

fn main() {
    println!("This is a no-op!");
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0207]: the type parameter `F` is not constrained by the impl trait, self type, or predicates
  --> src/lib.rs:41:9
   |
41 | impl<T, F> Handler<WorkerRequest, WorkerResult> for MyHandler<T>
   |         ^ unconstrained type parameter

error: aborting due to previous error

For more information about this error, try `rustc --explain E0207`.
error: could not compile `playground`

To learn more, run the command again with --verbose.

Disclaimer up front: I'm not well-versed in async, so there may be a better way. Others in the forum are and will likely correct me if needed.

Following the errors, though: using a type parameter in an associated type isn't considered to be constraining, and as the error says, type parameters of implementations must be constrained. Normally you could fix this by just naming the concrete type and not having a type parameter, but that isn't directly doable in this case because the generated future is unnameable (and the async "desugaring" is not actually implementable outside the compiler yet).

You also can't have -> impl Trait inside of a trait, which is probably why you had the associated type in the first place.

However, dyn Trait is a concrete, albeit unsized, type. You can return it behind some sort of indirection (which is Sized), like in a Box. Here I updated the playground to:

  • not have the Fut associated type
  • instead return Box<dyn Future<Output = Result<B, Self::Error>> + '_>
    • the '_ makes it take on the lifetime of &self instead of 'static
  • and updated the impl Handler<_,_> for MyHandler<T> accordingly

And that worked around the error.

Thanks for your response, @quinedot! I really appreciate it!

The thing is, the Handler trait in my playground was just to reproduce the issue. In the actual code, it comes from the lambda_runtime crate, and so it can't be modified.

For reference, here is the source of the Handler trait: https://github.com/awslabs/aws-lambda-rust-runtime/blob/797f0ab285fbaafe284f7a0df4cceb2ae0e3d3d4/lambda-runtime/src/lib.rs#L70

I have the exact same issue ? Did you find a solution or an example of a simple implementation of the Handler trait ?

You can implement the original Handler trait with a trait object Box<dyn Future<...>> instead of changing the trait as @quinedot did. It's doing the same thing but in the opposite direction, basically. There are a couple other issues that I ran into when making that change with your example so unfortunately it isn't a single change, but the others are mostly pretty minor.

Here's an updated playground with some comments explaining what the changes are there for. There are a couple ways you could go about solving the other errors that came up but I think this is pretty close to the simplest way to do it.

Code
use async_trait::async_trait;
use anyhow::Error;
use std::future::Future;
use std::pin::Pin;

trait Handler<A, B> {
    type Error;
    type Fut: Future<Output = Result<B, Self::Error>>;
    fn call(&self, request: A) -> Self::Fut;
}

struct WorkerRequest {
    id: String,
}

struct WorkerResult {
    message: String,
}

#[async_trait]
trait Worker {
    async fn do_work(&self, request: WorkerRequest) -> Result<WorkerResult, Error>;
}

struct MyHandler<T: Worker> {
    worker: T,
}

// The Clone bound is required to avoid referencing self in the future. There are other options that would avoid this bound but this is the most straightforward solution.
// The 'static bound is neccessary unless we introduce a lifetime another way (e.g. a PhantomData in MyHandler) though that can cause other problems.
impl<T: Worker + Clone + 'static> MyHandler<T> {
    fn new(worker: T) -> Self {
        MyHandler {
            worker: worker,
        }
    }
    
    fn work(&self, request: WorkerRequest) -> impl Future<Output = Result<WorkerResult, Error>> + 'static {
        // Handler::Fut doesn't give us a way to specify a lifetime easily, so we can't let the future reference self
        // by using a normal function that returns an impl Future instead of an async fn we can clone the worker when we create the future.
        let worker = self.worker.clone();
        
        // The async move block takes ownership of the cloned worker and builds a future with it.
        async move {
            let result = worker.do_work(request).await?;
            Ok(result)
        }
    }
}

impl<T> Handler<WorkerRequest, WorkerResult> for MyHandler<T>
where
    // Need the same bounds as the inherent impl on MyHandler
    T: Worker + Clone + 'static
{
    type Error = Error;
    type Fut = Pin<Box<dyn Future<Output = Result<WorkerResult, Error>>>>;

    fn call(&self, request: WorkerRequest) -> Self::Fut {
        // Create a trait object that is also pinned.
        // Pinning is usually only necessary when polling a future, but Box only implements Future when it's contents implement Future AND Unpin. Using Box::pin avoids that problem. 
        Box::pin(self.work(request))
    }
}

fn main() {
    println!("This is a no-op!");
}

The tl;dr is that having to use a trait object in your impl Handler introduces some extra requirements on the lifetimes your Future can contain. When async trait methods eventually reach stable this should hopefully get much easier.

Let me know if anything in there is confusing, or doesn't work the way you expect in the full context!

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.