`expected fn pointer ... found fn item ...` on a function pointer argument taking reference and returing future

This is a minimal reproducible example (link to playground) of my problem:

use std::future::Future;

struct Request {}

fn handle_requests<F>(handler: fn(&Request) -> F)
where
    F: Future<Output = ()>,
{}

async fn example_handler(req: &Request) {}

fn main() {
    handle_requests(example_handler);
}

In handle_requests, I am taking a function pointer which has a reference argument and returns a Future. The example_handler function has the right signature, but the code is rejected with this error:

error[E0308]: mismatched types
  --> src/main.rs:14:21
   |
14 |     handle_requests(example_handler);
   |     --------------- ^^^^^^^^^^^^^^^ one type is more general than the other
   |     |
   |     arguments to this function are incorrect
   |
   = note: expected fn pointer `for<'a> fn(&'a Request) -> _`
                 found fn item `for<'a> fn(&'a Request) -> impl Future<Output = ()> {example_handler}`
note: function defined here
  --> src/main.rs:5:4
   |
5  | fn handle_requests<F>(handler: fn(&Request) -> F)
   |    ^^^^^^^^^^^^^^^    --------------------------

I understand that the fn item type is the specific type of example_handler and the compiler is failing to cast it to the required fn pointer type, but I am struggling to understand why.

If I do not use a reference as argument, the problem goes away, so I think it's something to do with lifetimes.

Here, one type is more general than the other is referring to the fact that

  • your handle_requests signature requires fn(&Request) -> F i.e. for<'a>(&'a Request) -> F where the lifetime 'a does not appear in F, so that F is a type independent of the lifetime 'a, whereas
  • your function item async fn example_handler(req: &Request) has a signature where the returned future type does (have the ability to [1]) depend on the lifetime of the req parameter

so the type expected by handle_requests is “less general” in that dependence of the future type on the lifetime isn’t permitted.

Working around this issue in general is unfortunately a bit challenging due to compiler limitations. See this thread for more context.


  1. or actually… it will always depend on the parameter’s lifetime, sine async fn futures do nothing but capture all the function arguments before first being polled, hence they capture all lifetimes of all argument types ↩︎

3 Likes

This:

async fn example_handler(req: &Request) {}

Acts more or less like this:

// Just need something that mentions the lifetime in question
trait Captures<'a>  {}
impl<T: ?Sized> Captures<'a> for T {}

fn example_handler(req: &Request) -> impl Future<Output = ()> + Captures<'_> {
    async move {
        let req = req;
        // Function body here
    }
}

Which means that the return type is parameterized by the input lifetime -- that means that for every input lifetime 'x, you get a distinct output type (which differs only by lifetime, but types that differ by lifetime are still distinct types).

But here:

fn handle_requests<F>(handler: fn(&Request) -> F)
where
    F: Future<Output = ()>,

Type parameters like F must resolve to a single type. This can only work if the return type of handler does not capture the input lifetime -- if it's the same type for any input lifetime 'x.

For now at least, return position impl Trait (RPIT) doesn't automatically capture input lifetimes, so you can change example_handler to look like so:

fn example_handler(req: &Request) -> impl Future<Output = ()> {
    // Do anything that depends on `req` with it's non-`'static` lifetime here
    async move {
        // Don't and thus don't capture it here (maybe you don't need `move`)
    }
}

And then the single return type can work for you. If this is what you need, you should give your feedback in the tracking issue I linked above. To preserve this behavior, you'll need to eschew edition 2024 (unless they change their minds) or wait for TAIT to stabilize.

If this doesn't work for you, then you need a different signature than

fn handle_requests<F>(handler: fn(&Request) -> F)
where
    F: Future<Output = ()>,

You need one that doesn't use a type parameter like F for the return type of the function. There's ways to work around this too, though depending on your situation they don't always work without type erasure (Box<dyn Future<Output = ()> + Send + '_> or such) in an async setting, due to unnameable types and other issues.

trait MyTypeOfFuture<'a>: Fn(&'a Request) -> <Self as MyTypeOfFuture<'a>>::Out {
    type Out: Future<Output = ()>;
}

impl<'a, F: ?Sized, Out> MyTypeOfFuture<'a> for F
where
    F: Fn(&'a Request) -> Out,
    Out: Future<Output = ()>,
{
    type Out = Out;
}

// Note how the return type need not be named with a type parameter here
fn handle_requests<H: for<'any> MyTypeOfFuture<'any>>(handler: H)
{
}
3 Likes

Thank you both for the great answers!

So to summarize it seems I have 3 options:

  1. Use a trait to tell the compiler I expect the same lifetime for the input parameter and future.
  2. Box the future to erase the type.
  3. Modify my example_handler so it has independent lifetimes for parameter and future instead of the same.

I also found this solution (playground), which seems simpler for my case:

use std::future::Future;

struct Request {}

fn handle_requests<'a, F>(handler: fn(&'a Request) -> F)
where
    F: Future<Output = ()> + 'a,
{}

async fn example_handler(req: &Request) {}

fn main() {
    handle_requests(example_handler);
}

I just add a lifetime to indicate that my future has the same lifetime of the parameter. Is this a valid solution or am I missing something?

Well it's valid in some sense, but there's a good chance it won't be useful to you. Lifetimes parameters like 'a here:

fn handle_requests<'a, F>(handler: fn(&'a Request) -> F)

are chosen by the caller, and the caller can only choose lifetimes which are valid through the end of the function call or longer. In particular, that's always longer than the borrow of any variable local to handle_requests can be.[1] So if the caller isn't also supplying a &'a Request in some way,[2] it probably won't work for you. (If they are, it might work for you.)

The ways you say "you (the caller) have to support a borrow shorter than you can name -- like a borrow of a variable local to my function" are:

  • Higher-ranked trait bounds (HRTBs)
    • F: for<'a> ...
    • F: Fn(&Request) -> ... which is short for for<'a> Fn(&'a Request) -> ...
  • Higher-ranked types
    • fn(&Request) -> ... aka for<'a> fn(&'a Request) -> ...
    • Box<dyn Fn(&Request) -> ...> aka Box<dyn for<'a> Fn(&'a Request) -> ...>

  1. Or they could just choose 'static for that matter. ↩︎

  2. or you can produce a &'static Request somehow -- unlikely without leaking ↩︎

2 Likes

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.