Travis Brown
Travis Brown

Reputation: 139058

Using async functions that borrow arguments in contexts where trait object is needed

I've recently started playing with async streams in Rust, and I keep finding myself in situations where I want to use async functions in the implementation of a Stream. The async functions often come from libraries I don't control, but for the sake of example suppose they look like this:

async fn bar_str(s: &str) -> String {
    s.to_string()
}

async fn bar_string(s: String) -> String {
    s
}

Also to keep things simple suppose I'm just trying to use these functions to implement a trait like the following (no actual stream stuff involved):

use std::future::Future;

trait Foo {
    fn bar(self) -> Box<dyn Future<Output = String>>;
}

For the String case this just works as you'd expect:

impl Foo for String {
    fn bar(self) -> Box<dyn Future<Output = String>> {
        Box::new(bar_string(self))
    }
}

For the case where the async function borrows, it doesn't.

impl Foo for &str {
    fn bar(self) -> Box<Future<Output = String>> {
        Box::new(bar_str(self))
    }
}

This fails to compile:

error[E0495]: cannot infer an appropriate lifetime for lifetime parameter '_ in function call due to conflicting requirements
  --> foo.rs:23:18
   |
23 |         Box::new(bar_str(self))
   |                  ^^^^^^^^^^^^^
   |
...

I can understand why this is a problem, and I understand that the async fn syntax provides special handling for borrowed arguments like this (although I don't know anything about how it's actually checked, desugared, etc.).

My question is about what the best thing is to do in these situations generally. Is there some way I can reproduce the magic async fn is doing in my non-async fn code? Should I just avoid borrowing in async functions (when I can, since that's often a decision I didn't make)? In the code I'm currently writing I'm happy to use experimental or not-necessarily-future-proof solutions if they make reading and writing this kind of thing nicer.

Upvotes: 1

Views: 370

Answers (1)

rodrigo
rodrigo

Reputation: 98516

I think that the problem is not so much in the async but in the Box<dyn Trait> thing. In fact it can be reproduced with a simple trait:

use std::fmt::Debug;

trait Foo {
    fn foo(self) -> Box<dyn Debug>;
}

impl Foo for String {
    fn foo(self) -> Box<dyn Debug> {
        Box::new(self)
    }
}

impl Foo for &str {
    fn foo(self) -> Box<dyn Debug> {
        Box::new(self) // <--- Error here (line 15)
    }
}

The full error message is:

error[E0495]: cannot infer an appropriate lifetime due to conflicting requirements
  --> src/lib.rs:15:18
   |
15 |         Box::new(self)
   |                  ^^^^
   |
note: first, the lifetime cannot outlive the lifetime `'_` as defined on the impl at 13:14...
  --> src/lib.rs:13:14
   |
13 | impl Foo for &str {
   |              ^
note: ...so that the expression is assignable
  --> src/lib.rs:15:18
   |
15 |         Box::new(self)
   |                  ^^^^
   = note: expected `&str`
              found `&str`
   = note: but, the lifetime must be valid for the static lifetime...
note: ...so that the expression is assignable
  --> src/lib.rs:15:9
   |
15 |         Box::new(self)
   |         ^^^^^^^^^^^^^^
   = note: expected `std::boxed::Box<(dyn std::fmt::Debug + 'static)>`
              found `std::boxed::Box<dyn std::fmt::Debug>`

There is a nice hint about what is going on in the last two lines... what is this Box<(dyn Debug + 'static)> thing?

When you write dyn Trait there is actually an implicit 'static constraint to the type that implements that trait, so these two are the same thing:

Box<dyn Debug>
Box<(dyn Debug + 'static)>

But that means that we can only box a value whose type is 'static. And &'a str is not a static type, so it cannot be boxed that way.

The easy solution is, as usual, to clone, if at all possible. This compiles and it's not too ugly:

impl Foo for &str {
    fn foo(self) -> Box<dyn Debug> {
        Box::new(self.to_owned())
    }
}

Or if you only use static strings, then &'static str is actually static and you can write:

impl Foo for &'static str {
    fn foo(self) -> Box<dyn Debug> {
        Box::new(self)
    }
}

If you actually want or need to borrow, then the boxed dyn object has to be generic over some lifetime. You have to change the return type of your trait, something like this:

use std::fmt::Debug;

trait Foo<'a> {
    fn foo(self) -> Box<dyn Debug + 'a>;
}

impl Foo<'static> for String {
    fn foo(self) -> Box<dyn Debug> {
        Box::new(self)
    }
}

impl<'a> Foo<'a> for &'a str {
    fn foo(self) -> Box<dyn Debug + 'a> {
        Box::new(self)
    }
}

But beware now that Box<dyn Debug + 'a> is not a static type by itself, but the type itself has a lifetime of 'a.

Upvotes: 2

Related Questions