Why is the parameter type of the closure required to be explicitly specified?

Consider this example:

trait MyFn<T> {}
impl<T, F, R> MyFn<T> for F where F: Fn(T) -> R {}
fn call<F>(_: F)
where
    F: for<'a> MyFn<&'a str>,
{
}
fn main() {
    call(|s /*:&str*/| async {});
}

The compiler complains that:

error: implementation of `FnOnce` is not general enough
 --> src/main.rs:9:5
  |
9 |     call(|s /*:&str*/| async {});
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ implementation of `FnOnce` is not general enough
  |
  = note: closure with signature `fn(&'2 str) -> {async block@src/main.rs:9:24: 9:29}` must implement `FnOnce<(&'1 str,)>`, for any lifetime `'1`...
  = note: ...but it actually implements `FnOnce<(&'2 str,)>`, for some specific lifetime `'2`

However, if the comment is uncommented, the code will be compiled. In contrast, if directly using the Fn trait, the explicitly specified parameter type is not necessary.

fn call<F, U>(_: F)
where
    F: for<'a> FnOnce(&'a str) -> U,
    U: Future,
{
}
fn main() {
    call(|s /*:&str*/| async {});
}

This code is Ok. However, if assigning the closure to a variable and passing the variable as the argument, will get a similar error as above. What's the reason here?

1 Like

Inference of closures that take types with lifetimes is an input is poor (and if you return such an input too, even worse). This probably hasn't been improved because changing inference will break a lot of things.

For your example: outside of specific contexts that override the default inference, eliding the parameter type will result in the closure attempting to take a &str with one specific lifetime, but adding the type -- or even just the type constructor -- will make it attempt to take any lifetime (which is what you want in the example, but not always.)[1]

What specific contexts? When you're passing to a function annotated with the Fn traits specifically.[2] Adding an intermediate trait doesn't work, despite the supertrait; it's special behavior for the Fn traits only. I couldn't tell you why. Maybe they want to limit the scope of what's considered a hack.

The limitations are the motivation for RFC 3216 (not stable) and part of the motivation for RFC 3668 (stable, but has some shortcomings which I'm not yet intimately familiar with).


  1. And there is no handy workaround for taking any lifetime and returning the input. ↩︎

  2. and maybe some other annotated places ↩︎

1 Like