[Pre-RFC] Allow `for<'a>` syntax with closures for explicit higher-ranked lifetimes

Just to be sure my intentions are clear: I'm glad we're in agreement that it would be useful to explicitly indicate a non-higher-ranked lifetime on closure arguments, and I don't think I have any big concerns left. I wanted to be sure future compatibilities were considered, and there are a few things I'm not 100% clear about, but I'm happy with the gist of the RFC in general.

In other words, there's no real need to keep this thread going if it's a distraction from the Pre-RFC; just say so. But since you asked, I'll respond to your latest post.


The ability to force a non-higher-ranked argument is useful in any situation where a higher-ranked closure was inferred but a non-higher-ranked closure is needed.

Step by step example

To expand your example slightly, this:

    let mut fields: Vec<&str> = Vec::new();
    let pusher = |a| {
        println!("{:?}", a.len());
        fields.push(a);
    };

Give the error

error[E0282]: type annotations needed
  --> src/main.rs:28:19
   |
28 |     let pusher = |a| {
   |                   ^ consider giving this closure parameter a type
   |
   = note: type must be known at this point

But if you follow the advice

-    let pusher = |a| {
+    let pusher = |a: &str| {

You then get this

error[E0521]: borrowed data escapes outside of closure
  --> src/main.rs:30:9
   |
27 |     let mut fields: Vec<&str> = Vec::new();
   |         ---------- `fields` declared here, outside of the closure body
28 |     let pusher = |a: &str| {
   |                   - `a` is a reference that is only valid in the closure body
29 |         println!("{:?}", a.len());
30 |         fields.push(a);
   |         ^^^^^^^^^^^^^^ `a` escapes the closure body here

At which point you need the same kind of workarounds as forcing higher-ranked closures do.

    fn mk_pusher<'v, 'lt, F: FnMut(&'lt str) + 'v>(f: F) -> F { f }
    let mut fields: Vec<&str> = Vec::new();
    let pusher = mk_pusher(|a: &str| {
        println!("{:?}", a.len());
        fields.push(a);
    });

As for why one might want to use or force early bounds,

(collapsed as you've pointed out this doesn't apply to closures)

Whether it exists just due to a limitation or not, I don't think the early/late distinction is going anywhere.

For one, it's the other way around -- early bound allows turbofish (as the generic parameter is also a parameter of the type), and late bound does not. You make it early-bound if you want to be able to turbofish. #42868 and the impl Trait initiative emphasize that turbofish doesn't make sense with late-bound parameters. Making something unturbofish-able would at least be an edition change.

Moreover, inference isn't perfect, and sometimes you need to be able to specify lifetimes. On top of this, until there's a way to more finely distinguish early-bound and named-but-late-bound parameters, you might have to make all your parameters early-bound in order to do so. Arguably these are all just bugs to get rid of, but I don't think that's happening any time soon. (And suspect that there will always be something complicated enough to require an explicit mechanism to override inference, like turbofish.)

That's true -- and the analogous situation for closures would involve something like a TAIT or the nameable fn item types from the impl Trait initiative. In those cases I think there's always something else you can turbofish (parameterize) instead (an alias, a defining function, et cetra). In those contexts, you can name the parameterized lifetime:

let pusher = |a: &'lifetime_from_fn_def str| { /* ... */ }

No need to indirectly guide inference there.

(I say "think" because none of these things are stable... so it's all future compatibility territory.)

We're in agreement, and thanks again for clarifying the capturing of local region variables vs. parameters.

1 Like