I don't understand this lifetime error

Consider the following snippet of code:

fn create_closure() -> impl FnOnce(&mut usize) {
    let cloj = |a: &mut usize| {};

    cloj
}

This compiles. Now I thought, why even name the type of a, the compiler should be able to infer it from the function signature. So I did:

fn create_closure() -> impl FnOnce(&mut usize) {
    let cloj = |a| {};

    cloj
}

rust-analyzer confirms that the inferred type of a is &mut usize. However, this does not compile. The error is:

error: implementation of `FnOnce` is not general enough
 --> src/main.rs:4:5
  |
4 |     cloj
  |     ^^^^ implementation of `FnOnce` is not general enough
  |
  = note: closure with signature `fn(&'2 mut usize)` must implement `FnOnce<(&'1 mut usize,)>`, for any lifetime `'1`...
  = note: ...but it actually implements `FnOnce<(&'2 mut usize,)>`, for some specific lifetime `'2`

It seems that the lifetime of the closure is different when I name the type of the argument vs when I don't. However, in the error, the compiler seems to be able to to tell me "what kind of type/lifetime I need", so why does it not infer it then?

Can someone explain in detail what's happening here? Would love to learn more about this.

Thanks!

See Nerditation's and Quinedot's answers for a similar post here.

In short: removing the lifetime from the closure's parameter erases the syntax sugar for the higher ranked trait bounds, resulting in a closure that expects a single, specific lifetime.

1 Like

The inference of a closure's arguments and return type can be driven by a Fn-family trait bound on the place where the closure is defined. Usually that is when you're passing the closure to a generic function. But in this case, your -> impl FnOnce(&mut usize) will drive the closure inference when the closure is defined in a return position.

// This compiles: The `-> impl FnOnce` determines how the compiler interpretets
// the closure's argument and return type.
fn create_closure() -> impl FnOnce(&mut usize) {
    |a| {}
}

When closure inference is not being driven by a Fn-family bound, it's pretty... not great when it comes to anything with a lifetime. If you have an annotated-but-elided lifetime in the arguments, the compiler will decide you want to take accept any lifetime there. But if there's a lifetime in the arguments and it's not annotated, it will decide you only want to accept one specific lifetime.[1]

So this also works...

fn create_closure() -> impl FnOnce(&mut usize) {
    // No longer in return position, you get default closure inference.
    //
    // The presence of `&mut` alone is enough to fix the bad inference.
    let cloj = |a: &mut _| {};
    
    cloj
}

...but as you've discovered, leaving the argument completely unannotated does not work.[2]

And the situation is even worse with the return type; there's no inline partial work-around like there is for arguments. Without using the Fn-family bound trick, this just won't work, because the compiler will think it's supposed to infer one specific lifetime for the return type, instead of making it the same as the input lifetime.

// Fails to compile
fn create_closure() -> impl FnOnce(&mut usize) -> &mut usize {
    // Note that even though this would mean what you want if you were
    // writing a `fn(&mut usize) -> &usize` -- or a bound, like in the
    // signature above -- it doesn't have the same effect for closures!
    let cloj = |a: &mut usize| -> &mut usize { a };
    cloj
}

So we use hacks like this...

// Compiles
fn create_closure() -> impl FnOnce(&mut usize) -> &mut usize {
    // Identity function with a bound to give us the better inference
    fn sigh<F: FnOnce(&mut usize) -> &mut usize>(f: F) -> F { f }
    let cloj = sigh(|a| a);
    cloj
}

...unless the types are unnameable (like closures and most futures), defeating our ability to use the workaround, at which point we cry.

As for this part, some part of the compiler process would have to be re-ran after borrow check fails. Perhaps it's not practical, or perhaps the burden to make it consistently draw the same conclusion is too high to commit to. There are many situations where an error diagnostic can figure out the problem, but the compiler or language itself doesn't automatically accept the code as-is.

(We can't change closure inference to assume "make this any lifetime", etc, on a whim. That would break the use cases where the programmer actually needed a single lifetime; inference changes are very hard to make without breaking too much. Perhaps it could change over an edition.)


  1. There are actually use cases that require this... though it's not very common I think. ↩︎

  2. Neither does a: _. You need the spot where the lifetime would go to be present in the annotation. ↩︎

5 Likes

Interesting, thank you very much for the detailed answer!!

there's also a nightly feature that lets you use for<'a> on the closure: Rust Playground

#![feature(closure_lifetime_binder)]
pub fn create_closure() -> impl FnOnce(&mut usize) -> &mut usize {
    let cloj = for<'a> |a: &'a mut usize| -> &'a mut usize { a };
    cloj
}
2 Likes

This is interesting. Is this the correct tracking issue? Tracking Issue for RFC 3216: "Allow using `for<'a>` syntax when declaring closures" · Issue #97362 · rust-lang/rust · GitHub

If so, it doesn't seem like this is going anywhere? The issue was actually closed as far as I can see.

The issue is still open, but locked. This doesn’t mean the work is done or cancelled, but that the issue was attracting too many off-topic comments — problems with an unstable feature are supposed to be filed as separate issues, and the tracking issue itself is for implementation progress and stabilization.