Creating a trait with functions that return Future

I'm trying to create a trait that contains functions that return Future with the goal of being able to implement different versions that use different Future implementations such as Ready for synchronous behaviors. What I've gotten so far is this:

pub trait Cache {
    fn get(&self, key: String) -> dyn Future<Output=Option<String>>;
    fn set(&self, key: String, value: String) -> dyn Future<Output=()>;
}

struct NullCache;

impl Cache for NullCache {
    fn get(&self, key: String) -> Ready<Option<String>> {
        ready(None)
    }

    fn set(&self, key: String, value: String) -> Ready<()> {
        ready(())
    }
}

this results in:

error[E0053]: method `get` has an incompatible type for trait
  --> src/cache.rs:26:35
   |
26 |     fn get(&self, key: String) -> Ready<Option<String>> {
   |                                   ^^^^^^^^^^^^^^^^^^^^^
   |                                   |
   |                                   expected trait object `dyn futures_util::Future`, found struct `std::future::Ready`
   |                                   help: change the output type to match the trait: `(dyn futures_util::Future<Output = std::option::Option<std::string::String>> + 'static)`
   |
note: type in trait
  --> src/cache.rs:19:35
   |
19 |     fn get(&self, key: String) -> dyn Future<Output=Option<String>>;
   |                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   = note: expected fn pointer `fn(&NullCache, std::string::String) -> (dyn futures_util::Future<Output = std::option::Option<std::string::String>> + 'static)`
              found fn pointer `fn(&NullCache, std::string::String) -> std::future::Ready<std::option::Option<std::string::String>>`

How should I define the Future return types in the trait so that Ready<> and other future implementations are matching output types?

The futures crate's BoxFuture can be used to create type-erased futures. Here, it can be used as a return type for the trait methods (Rust Playground):

use futures::future::{ready, BoxFuture};

pub trait Cache {
    fn get(&self, key: String) -> BoxFuture<'static, Option<String>>;
    fn set(&self, key: String, value: String) -> BoxFuture<'static, ()>;
}

struct NullCache;

impl Cache for NullCache {
    fn get(&self, _key: String) -> BoxFuture<'static, Option<String>> {
        Box::pin(ready(None))
    }

    fn set(&self, _key: String, _value: String) -> BoxFuture<'static, ()> {
        Box::pin(ready(()))
    }
}

Thanks! That solves my immediate problem.

I'm a Rust newbie. If you can tell me or point me to some appropriate documentation why the use of something like BoxFuture is required for the return type, rather than just the plain trait, I'd appreciate it. I assume it has to do with lifetimes, but all that is still fairly mysterious to me.

dyn Future<Output=()> is a so-called “unsized” type; the simplest way to think about this is that you cannot really use such a type on its own at all in most situations, and you’ll instead always have to put it behind some pointer type. In particular, you can’t return an unsized type from a function. Options for pointer types include references, Box (in general also Rc/Arc, and some more). So Box<dyn Future<Output=()>>, &dyn Future<Output=()> or &mut dyn Future<Output=()> all are ordinary types.

The most common examples of unsized types are slices like [i32] or string slices like str. Those, too, always need some pointer indirection in order work with them, so &[i32], &mut [i32] or Box<i32> are option; or &str, &mut str, Box<str> (or even things like Rc<str>).

BoxFuture does contain some more stuff beyond the Box: There’s also the Pin, and there’s also the + Send + 'a.

  • The lifetime is just a minor detail with dyn SomeTrait types; it can be left out and gets some default values, for Box<dyn SomeTrait>, that’s falling back to Box<dyn SomeTrait + 'static>, so the use of 'static in @LegionMammal978's code is a good starting point, but allowing other lifetimes can be beneficial (and might even be necessary for this case for certain trait implementations; feel free to ask follow-up questions if you encounter lifetime issues).
  • The + Send is for convenience, anticipating that the future is going to be, potentially, be spawned as (part of) some task in a multi-threaded executor, e.g. via tokio’s spawn function, in which case a Send bound would be necessary.
  • The Pin is a very Future-specific detail; for technical reasons, only a Pin<Box<dyn Future<…>>> type is going to be a Future itself, which allows you to .await the whole thing easily, or spawn it, etc…, while Box<dyn Future<…>> isn’t a Future.

Here’s some more information on unsized types in the book: Advanced Types - The Rust Programming Language


For the particular case of future-returning functions in traits, you could the popular async-trait crate, which inserts a BoxFuture underneath, and does the boxing (and creation of the future via async {} blocks) for you. It also solves some lifetime problems that I eluded to that could otherwise come up if you use BoxFuture<'static, …> but tried to use self in that future.

The ready calls can be replaced by async blocks…

impl Cache for NullCache {
    fn get(&self, _key: String) -> BoxFuture<'static, Option<String>> {
        Box::pin(async { None })
    }

    fn set(&self, _key: String, _value: String) -> BoxFuture<'static, ()> {
        Box::pin(async {})
    }
}

and this is (somewhat) close to what (the trait impl in) the following use of async-traits’s macro translates into.

use async_trait::async_trait;

#[async_trait]
pub trait Cache {
    async fn get(&self, key: String) -> Option<String>;
    async fn set(&self, key: String, value: String);
}

struct NullCache;

#[async_trait]
impl Cache for NullCache {
    async fn get(&self, _key: String) -> Option<String> {
        None
    }

    async fn set(&self, _key: String, _value: String) {}
}
7 Likes

Awesome! That really helps understanding how this works