How to accept an `async fn` as an argument?

What does the error mean? The only difference is the for<'a> part but I couldn't make sense of it.

use std::future::Future;

type FutResult = Result<(), Box<dyn std::error::Error>>;

struct Dummy;

async fn parser(_: &Dummy) -> FutResult {
    Ok(())
}

fn crawler<F, Fut>(_: F)
where
    F: FnOnce(&Dummy) -> Fut,
    Fut: Future<Output = FutResult>,
{
}

#[tokio::main]
async fn main() {
    crawler(parser);
}
error[E0308]: mismatched types
  --> src/main.rs:20:5
   |
20 |     crawler(parser);
   |     ^^^^^^^^^^^^^^^ one type is more general than the other
   |
   = note: expected opaque type `impl for<'a> Future<Output = Result<(), Box<(dyn std::error::Error + 'static)>>>`
              found opaque type `impl Future<Output = Result<(), Box<(dyn std::error::Error + 'static)>>>`
   = help: consider `await`ing on both `Future`s
   = note: distinct uses of `impl Trait` result in different opaque types
note: the lifetime requirement is introduced here
  --> src/main.rs:13:26
   |
13 |     F: FnOnce(&Dummy) -> Fut,
   |                          ^^^

For more information about this error, try `rustc --explain E0308`.
2 Likes

You can do it like this:

fn crawler<'a, F, Fut>(_: F)
where
    F: FnOnce(&'a Dummy) -> Fut,
    Fut: Future<Output = FutResult>,
{
  
}
1 Like

If I'm not mistaken, since you didn't specify what lifetime the argument to the FnOnce had, then it has to work for any lifetime (that's what for<'a> means). But you passed a reference with a concrete lifetime; that's why the compiler expects a "more general type".

Adding a generic lifetime allows the compiler to monomorphizate a version of the function for the concrete lifetime of your parameter, and this is why @moy2010's code compiles.

PS. I probably used the terms in a sloppy way; hopefully this helps you. I'd also be grateful if somebody corrects my inaccuracies, so I can learn more!

3 Likes

Explanation of the problem

The outputs of async fn capture all of their generic parameters. If we make the generic parameters of parser explicit, it looks like so:

async fn parser<'p>(_: &'p Dummy) -> FutResult

And it acts like FutResult is also parameterized by 'p, and thus the output type varies based on the lifetime parameter. That is, types which differ by lifetime are distinct types, even if they only differ by lifetime.

So for any input lifetime 'p, you get a different output type.

Then here:

fn crawler<F, Fut>(_: F)
where
    F: FnOnce(&Dummy) -> Fut,
    // The above could also be written:
    // F: for<'any> FnOnce(&'any Dummy) -> Fut,
    Fut: Future<Output = FutResult>,

Type parameters like Fut have to resolve to a single type. parser can't meet this bound because it doesn't output the same type across all the possible input lifetimes. We don't have impl Trait in bounds nor any other direct way to show that relationship in bounds unless you can spell out the parameterized type.[1] There's no way to do that with opaque types yet, either.

So that's why parser can't meet the bound on crawler (and why there's no simple way to fix it without changing what the bound means).

Possible Solution: Single lifetime bound

If you restrict the bound to a single input lifetime as @moy2010 suggested, then the output type can resolve to a single type and the example compiles. This will only work for your use case if the caller can select the lifetime (e.g. they are supplying the &'a Dummy to be pased to the FnOnce(&'a Dummy)).

Possible Solution: Boilerplate ahoy

If you need to support any input lifetime to parser in crawler (e.g. you'll be passing in a local borrow), then the single lifetime bound won't be sufficient. However, there are some workarounds. If the output of parser does actually need to capture the lifetime of the &Dummy, you'll need some boilerplate (or type erasure, which we'll get to shortly).

You can indirectly spell out the borrowing relationship by way of a custom trait, and then bound on that trait in such a way that you don't have to mention the output type (don't have to try to resolve it with a type parameter like Fut).

trait ParserOne<'a> {
    type OutOne: Future<Output = FutResult>;
    fn parse_one(self, _: &'a Dummy) -> Self::OutOne;
}

// It's ok to use `Fut` here since it's only for one input lifetime
impl<'a, F, Fut> ParserOne<'a> for F where 
where
    F: FnOnce(&'a Dummy) -> Fut,
    Fut: Future<Output = FutResult>,
{ ... }

trait Parser: for<'any> ParserOne<'any> {
    type Out<'a>: Future<Output = FutResult>;
    fn parse(self, dummy: &Dummy) -> Self::Out<'_>;
}

impl<F: for<'any> ParserOne<'any>> Parser for F { ... }

And change your bounds elsewhere.

fn crawler<F: Parser>(_: F) {
}

Possible Solution: Boxed futures

If you don't want all that boilerplate and don't mind the hit from boxing and type erasing, it looks like so.

fn parser(_: &Dummy) -> Pin<Box<dyn Future<Output = FutResult> + '_>> {
    Box::pin(async move {
        Ok(())
    })
}

fn crawler<F>(_: F)
where
    F: FnOnce(&Dummy) -> Pin<Box<dyn Future<Output = FutResult> + '_>>,
{
}

Or perhaps only change the bound on crawler and call it like

#[tokio::main]
async fn main() {
    crawler(|d| Box::pin(parser(d)));
}

By erasing the opaque future type, we can "spell out" the return type (Pin<Box<...>>).

Possible Solution: Don't capture the input lifetime

If you never need to capture the lifetime (so the output type actually is the same for any input lifetime), you can use return-position impl trait (RPIT) instead of async fn and you won't have to change the bound on crawler. This works because RPIT doesn't automatically capture generic lifetimes that aren't mentioned in the return type (even though it still does capture generic types).

fn parser(_: &Dummy) -> impl Future<Output = FutResult> {
    async move {
        Ok(())
    }
}

However, this will probably change in the next edition. In which case, you may end up stuck on edition 2021 until they stabilize TAIT or some other way to get the behavior back.

If you have a concrete use case for this pattern, you should post it in the tracking issue.


  1. example: F: FnOnce(&Dummy) -> Vec<&Fut>, where the & is "spelled out"; also, the Pin<Box<...>> approach below ↩ī¸Ž

8 Likes

Why does FutResult also parameterized by 'p? I don't see lifetime eliision here and FutResult doesn't have any reference involed.

With elision, I don't see FutResult is related to lifetime?

type FutResult = Result<(), Box<dyn std::error::Error>>;

async fn parser(_: &Dummy) -> FutResult               // elided
async fn parser<'p>(_: &'p Dummy) -> FutResult    // expanded
1 Like

It's not the FutResult itself that is parametrized, it's the future wrapping it.

Essentially, async functions are desugared to something like this:

fn parser<'arg>(_: &'arg Dummy) -> impl Future<Output = FutResult> + 'arg {
    async move {
        Ok(())
    }
}

Note that FutResult doesn't have any lifetimes in it (as it should), but impl Future does - and again, as it should in general, since it could capture &'arg Dummy (nothing in the signature prohibits it).

5 Likes

More technically, behind the scenes an async fn is defined in terms of an unstable feature called TAIT (type alias impl Trait), and the notionaly desugaring of

async fn foo(arg: &Dummy) -> FutResult { /* body */ }

is

// This declares an alias for a type constructor, potentially otherwise
// opaque/unnameable; its use as in return position elsewhere defines
// what the aliased types are
type _OpaqueFooReturn<'a> = impl Future<Output = FutResult>;

// The function body of `foo` supplies the definition, just like RPIT
// The (invisible) mentioned elision    vv
fn foo(arg: &Dummy) -> _OpaqueFooReturn<'_> {
    async move {
        let (arg,) = (arg,);
        /* body */
    }
}

And more generally (with fake variadic generics and arguments syntax)

async fn bar<AllGenericsImplicitOrNot, ..>(arg, ..) -> Ret { $body }
// =>
type _OpaqueBarReturn<AllGenericsImplicitOrNot, ..> 
    = impl Future<Output = Ret>;

fn bar<AllGenericsImplicitOrNot, ..>(arg, ..)
    -> _OpaqueBarReturn<AllGenericsImplicitOrNot, ..>
{
    async move {
        (arg, ..) = (arg, ..);
        $body
    }
}

Which is subtly different than impl ... + 'a. More information/discussion in this recent thread.


All that being said, impl Future<Output = FutReturn> + '_ is an adequate way to think of the notional desugaring for the purposes of understanding the OP error.

1 Like