3

I'd like to write a function which takes one argument: a pointer to an async function taking a lifetime parameter.

Here's a minimal example:

use std::future::Future;

async fn takes_i32_ref<'a>(r: &'a i32) { }

fn takes_fn<F: Future>(f: for<'a> fn(&'a i32) -> F) { }

fn main() {
    takes_fn(takes_i32_ref);
}

When I try to run this I get the message:

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

I found a question that seemed pretty related: Why isn't `std::mem::drop` exactly the same as the closure |_|() in higher-ranked trait bounds?

I think that question is different from mine though. In that question, a for<'a> FnOnce<(&'a &str,)> is expected and a FnOnce<(&&str,)> is provided. The provided type is less general than the expected type because it lacks a for.

However, my example says that a for<'a> fn(&'a i32) -> _ is expected and a for<'a> fn(&'a i32) -> impl for<'a> Future<Output = ()> {takes_i32_ref} is provided. The provided type is more general than the expected type because it has a for.

Why can't Rust do this coercion?

Edit: I should also add that this error doesn't appear when I get rid of the reference in the signature of takes_i32_ref and f. That the async function takes a parameter with a lifetime seems to be important.

jrpear
  • 232
  • 2
  • 6
  • Is the goal here to define a function that takes a function argument that returns a Future? – tadman Oct 07 '22 at 04:37
  • 1
    I'm trying to define a function that takes an async function that takes a parameter with a lifetime. My understanding is that yes, that means I'm trying to define a function that takes a function argument that returns a future. – jrpear Oct 07 '22 at 16:21

1 Answers1

2

The future returned by an async function is only valid for as long as the arguments are. Therefore, the return type may depend on the argument lifetimes. So, per the original RFC, the type of takes_i32_ref is really something like

fn takes_i32_ref<'a>(r: &'a i32) -> impl Future<Output = ()> + 'a

Now it should be clear why your code doesn't work: takes_fn's F cannot depend on 'a; you've written that takes_fn takes a function that, for all 'a, returns the same type F. takes_i32_ref is not such a function, so takes_fn(takes_i32_ref) fails.

In an alternate universe/future Rust, this might be fixable with higher-kinded polymorphism:

// imaginary syntax
fn takes_fn<F<'a>: Future + 'a>(f: for<'a> fn(&'a i32) -> F<'a>) { }
// which would be the same as (also imaginary, but at least this one parses...)
fn takes_fn(f: for<'a> fn(&'a i32) -> (impl Future + 'a)) { }

In current nightly Rust, you can create a trait containing an async fn and then implement that. (This is possible since traits now support generic associated types, which is very close to higher kinded polymorphism.).

#![feature(async_fn_in_trait)]

trait TakesI32Ref { // equivalent to the imaginary trait for<'a> Fn(&'a i32) -> (impl Future + 'a), not really a function pointer as you wrote
    async fn call<'a>(&self, r: &'a i32) -> ();
}
enum Impl { ImplVal }
impl TakesI32Ref for Impl {
    async fn call<'a>(&self, _r: &'a i32) { }
}
fn takes_fn<F: TakesI32Ref>(f: F) {
    let x: i32 = 5;
    f.call(&x);
}
fn main() {
    takes_fn(Impl::ImplVal);
}

To force it to work in stable, for a nonzero performance penalty, you can use dyn.

fn takes_fn<O>(f: for<'a> fn(&'a i32) -> Box<dyn Future<Output = O> + 'a>) { }

fn main() {
    takes_fn(|r| Box::new(takes_i32_ref(r)));
}
HTNW
  • 27,182
  • 1
  • 32
  • 60