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

Summary

Allow explicitly specifying lifetimes on closures via for<'a> |arg: &'a u8| { ... }. This will always result in a higher-ranked closure which can accept any lifetime (as in fn bar<'a>(val: &'a u8) {}). Closures defined without the for<'a> syntax retain their current behavior: lifetimes will be inferred as either some local region (via an inference variable), or a higher-ranked lifetime.

Motivation

There are several open issues around closure lifetimes (Confusing compiler error for function taking closure returning reference · Issue #91966 · rust-lang/rust · GitHub and Confusing type error due to strange inferred type for a closure argument · Issue #41078 · rust-lang/rust · GitHub), all of which stem from type inference incorrectly choosing either a higher-ranked lifetime, or a local lifetime.

This can be illustrated in the following cases:

  1. We infer a higher-ranked region ( for<'a> fn(&'a u8) ) when we really want some specific (local) region. This occurs in the following code:
fn main () {
    let mut fields: Vec<&str> = Vec::new();
    let pusher = |a: &str| fields.push(a);
}

which gives the error:

error[E0521]: borrowed data escapes outside of closure
 --> src/main.rs:3:28
  |
2 |     let mut fields: Vec<&str> = Vec::new();
  |         ---------- `fields` declared here, outside of the closure body
3 |     let pusher = |a: &str| fields.push(a);
  |                   -        ^^^^^^^^^^^^^^ `a` escapes the closure body here
  |                   |
  |                   `a` is a reference that is only valid in the closure body

The issue is that Vec<&str> is not higher-ranked, so we can only push an &'0 str for some specific lifetime '0 . The pusher closure signature requires that it accept any lifetime, which leads to a compiler error.

  1. We infer some specific region when we really want a higher-ranked region. This occurs in the following code:
use std::cell::Cell;

fn main() {
    let static_cell: Cell<&'static u8> = Cell::new(&25);
    let closure = |s| {};
    closure(static_cell);
    let val = 30;
    let short_cell: Cell<&u8> = Cell::new(&val);
    closure(short_cell);
}

I'm using Cell to force invariance, since otherwise, region subtyping will make this example work even without a higher-ranked region. The above code produces the following error:

error[E0597]: `val` does not live long enough
  --> src/main.rs:8:43
   |
4  |     let static_cell: Cell<&'static u8> = Cell::new(&25);
   |                      ----------------- type annotation requires that `val` is borrowed for `'static`
...
8  |     let short_cell: Cell<&u8> = Cell::new(&val);
   |                                           ^^^^ borrowed value does not live long enough
9  |     closure(short_cell);
10 | }
   | - `val` dropped here while still borrowed

Here, the closure gets inferred to |s: Cell<&'static u8>| , so it cannot accept a Cell<&'0 u8> for some shorter lifetime &'0 . What we really want is for<'a> |s: Cell<&'a u8>| , so that the closure can accept both Cell s.

It might be possible to create an 'ideal' closure lifetime inference algorithm, which always correctly decides between either a higher-ranked lifetime, or some local lifetime. Even if we were to implement this, however, the behavior of closure lifetimes would likely remain opaque to the majority of users. By allowing users to explicitly 'desugar' a closure, we can make it easier to teach how closures work. Users can also take advantage of the for<> syntax to explicitly indicate that a particular closure is higher-ranked - just as they can explicitly provide a type annotation for the parameters and return type - to improve the readability of their code.

Guide-level explanation

When writing a closure, you will often take advantage of type inference to avoid the need to explicitly specify types. For example:

fn func(_: impl Fn(&i32) -> &i32) {}

fn main() {
    func(|arg| { arg });
}

Here, the type of arg will be inferred to &i32, and the return type will also be &i32. We can write this explicitly:

fn func(_: impl Fn(&i32) -> &i32) {}

fn main() {
    func(|arg: &i32| -> &i32 { arg });
}

Notice that we've elided the lifetime in &i32. When a lifetime is written this way, Rust will infer its value based on how it's used.

In this case, our closure needs to be able to accept an &i32 with any lifetime. This is because our closure needs to implement Fn(&i32) -> &i32 - this is syntactic sugar for for<'a> Fn(&'a i32) -> &'a i32.

We can make this explicit by writing our closure in the following way:

fn func(_: impl Fn(&i32) -> &i32) {}

fn main() {
    func(for<'a> |arg: &'a i32| -> &'a i32 { arg });
}

This indicates to both the compiler and the user that this closure can accept an &i32 with any lifetime, and returns an &i32 with the same lifetime.

However, there are cases where a closure cannot accept any lifetime - it can only accept some particular lifetime. Consider the following code:

fn main() {
    let mut values: Vec<&bool> = Vec::new();
    let first = true;
    values.push(&first);
     
    let mut closure = |value| values.push(value);   
    let second = false;
    closure(&second);
}

In this code, closure takes in an &bool, and pushes it to values. However, closure cannot accept an &bool with *any* lifetime - it can only work with some specific lifetime. To see this, consider this slight modification of the program:

fn main() {
    let mut values: Vec<&bool> = Vec::new();
    let first = true;
    values.push(&first);
    
    let mut closure = |value| values.push(value);    
    { // This new scope was added
        let second = false;
        closure(&second);
    } // The scope ends here, causing `second` to be dropped
    println!("Values: {:?}", values);
}

This program fails to compile:

error[E0597]: `second` does not live long enough
  --> src/main.rs:9:17
   |
9  |         closure(&second);
   |                 ^^^^^^^ borrowed value does not live long enough
10 |     }
   |     - `second` dropped here while still borrowed
11 |     println!("Values: {:?}", values);
   |                              ------ borrow later used here

This is because closure can only accept an &bool with a lifetime that lives at least as long as values. If this code were to compile (that is, if closure could accept a &bool with the shorter lifetime associated with &second), then values would end up containing a reference to the freed stack variable second.

Since closure cannot accept any lifetime, it cannot be written as for<'a> |value: &'a bool| values.push(value). It's natural to ask - how can we write down an explicit lifetime for value: &bool?

Unfortunately, Rust does not currently allow the signature of such a closure to be written explicitly. Instead, you must rely on type inference to choose the correct lifetime for you.

Reference-level explanation

We now allow closures to be written as for<'a .. 'z>, where 'a .. 'z is a comma-separated sequence of zero or more lifetimes. The syntax is parsed identically to the for<'a .. 'z> in the function pointer type for<'a .. 'z> fn(&'a u8, &'b u8) -> &'c u8

When this syntax is used, any lifetimes specified with the for<> binder are always treated as higher-ranked, regardless of any other hints we discover during type inference. That is, a closure of the form for<'a, 'b> |first: &'a u8, second: &'b bool| -> &'b bool will have a compiler-generated impl of the form:

impl<'a, 'b> FnOnce(&'a u8, &'b bool) -> &'b bool for [closure type] }

Using this syntax requires that the closure signature be fully specified, without any elided lifetimes or implicit type inference variables. For example, all of the following closures do not compile:

for<'a> |elided: &u8, specified: &'a bool| -> () {}; // Compiler error: lifetime in &u8 not specified
for<'b> || {}; // Compiler error: return type not specified
for<'c> |elided_type| -> &'c bool { elided_type }; // Compiler error: type of `elided_type` not specified
for<> || {}; // Compiler error: return type not specified

This restriction allows us to avoid specifying how elided lifetime should be treated inside a closure with an explicit for<>. We may decide to lift this restriction in the future.

Drawbacks

This slightly increases the complexity of the language and the compiler implementation. However, the syntax introduced (for<'a>) can already be used in both trait bounds and function pointer types, so we are not introducing any new concepts in the languages.

In its initial form, this feature may be of limited usefulness - it can only be used with closures that have all higher-ranked lifetimes, prevents type elision from being used, and does not provide a way of explicitly indicating non-higher-ranked lifetimes. However, this proposal has been explicitly designed to be forwards-compatible with such additions. It represents a small, (hopefully) uncontrovertial step towards better control over closure signatures.

Rationale and alternatives

  • We could use a syntax other than for<> for binding lifetimes - however, this syntax is already used, and has the same meaning here as it does in the other positions where it is allowed.
  • We could allow mixing elided and explicit lifetimes in a closure signature - for example, for<'a> |first: &'a u8, second: &bool|. However, this would force us to commit to one of two options for the interpetation of second: &bool
  1. The lifetime in &bool continues to be inferred as it would be without the for<'a>, and may or may not end up being higher-ranked.
  2. The lifetime in &bool is always non-higher-ranked (we create a region inference variable). This would allow for solving the closure inference problem in the opposite direction (a region is inferred to be higher-ranked when it really shouldn't be).

These options are mutually exclusive. By banning this ambiguous case altogether, we can allow users to begin experimenting with the (limited) for<> closure syntax, and later reach a decision about how (or not) to explicitly indicate non-higher-ranked regions.

  • We could try to design a 'perfect' or 'ideal' closure region inference algorithm that always correctly chooses between a higher-ranked and non-higher-ranked region, eliminating the need for users to explicitly specify their choice. Even if this is possible and easy to implement, there's still value in allowing closures to be explicitly desugared for teaching purposes. Currently: function definitions, function pointers, and higher-ranked trait bounds (e.g. Fn(&u8)) can all have their lifetimes (mostly) manually desugared - however, closures do not support this.
  • We could do nothing, and accept the status quo for closure region inference. Given the number of users that have run into issues in practice, this would mean keeping a fairly significant wart in the Rust language.

Prior Art

I previously discussed this topic in Zulip: rust-lang

The for<> syntax is used with function pointers (for<'a> fn(&'a u8)) and higher-ranked trait bounds (fn bar<T>() where for<'a> T: Foo<'a> {})

I'm not aware of any languages that have anything analogous to Rust's distinction between higher-ranked and non-higher-ranked lifetimes, let alone an interaction with closure/lambda type inference.

Unresolved questions

None at this time

Future possibilities

We could allow a lifetime to be explicitly indicated to be non-higher-ranked. The '_ lifetime could be given special meaning in closures - for example, for<'a> |first: &'a u8, second: &'_ bool| {} could be used to indicate a closure that takes in a &u8 with any lifetime, and an &bool with some specific lifetime. However, we already accept |second: &'_ bool| {} as a closure, so this would require changing the behavior of &'_ when a for<> binder is present.

25 Likes

About the syntax, why use the explicit for<'a, 'b> |first: &'a u8, second: &'b bool| -> &'b instead of |first: &'a u8, second: &'b bool| -> &'b as we did with fn signatures?

Today with functions,

// This is higher-ranked (late bound)
fn f1(_: &str) {}
// So is this
fn f2<'a>(_: &'a str) {}
// But if it is part of clause, it is not (it is early bound)
fn g1<'a: 'b>(_: &'a str, _: &'b str) {}
// Including silly ones for when you need to force things
fn g2<'a: 'a>(_: &'a str) {}

That is, eliding lifetimes results in a late bound, and more syntax is required to force early-bound. Thus, my first impression was that it would be too bad if closures ended up being the opposite (either permanently, or in some sense Rust would have to support forever).

But given that the suggested syntax lines up with the fn f2<'a> example above, the interpretation doesn't seem too bad; I can think of for<> || as just giving the closure a place to declare (limited) generics. However, there are still some future compatibility considerations:

  • If closures gain more generic abilities, putting them into for<> may not line up with the other uses of for<>
  • If we gain bounded for<'a: 'b, 'b> fn(&'a str, &'b str) types, will those line up with closures? (I don't have a citation on hand, but I've seen Niko comment on the possibility of such bounded higher-ranked types before.)

Also, how feasible is a syntax for forcing early-bound on closures as well (or instead)? Then, over some edition, the higher-ranked-ness could be made the default. (In the Rationale you imply this isn't a choice, listing only inference or non-higher-ranked, but I don't understand why it can't work.) The lack of such a syntax seems to be recognized as a shortcoming of this RFC regardless:

(for<'a: 'a> would be one such syntax consistent with functions today... but inconsistent with other uses of for<>, ala the bullet consideration above.)

Incidentally, I'm I right in concluded that this means the "rustc chose to make it higher-ranked but shouldn't have" case is unaddressed?


Nit: fresh output lifetimes must also appear as an input lifetime in fn types and Fn bounds/dyn types (though this is not true for unnameable function types).


Other alternatives

impl Trait in let, which is already approved as part of RFC 2071, can handle that use case:

    let static_cell: Cell<&'static u8> = Cell::new(&25);
    let closure: impl Fn(Cell<&u8>) = |s| {};
    // ...

However, it can't handle other places that also fail today (without rewriting things to assign to a variable).

But, if expression type ascription stabilizes (or generalized type ascription comes back), impl Trait could handle them:

    fn_expecting_higher_ranked_option(Some(|s| {} : impl Fn(&str))); 

(RPIT can technically do this on stable today. No, I'm not saying it's reasonable to do so.)


And dyn for<'a> Fn(&'a())>-like types.

5 Likes

If you mean "in-band lifetimes" that don't require a declaration, those were recently retired until further consideration for fn signatures.

4 Likes

You don't even need RPIT, generic bounds are enough. This has the advantage (compared to RPIT) to be reusable, but it's still not that reasonable (though it's probably what some people are doing).

2 Likes

Can you elaborate on what those extra abilities might look like?

That seems reasonable - are you anticipating any problems with applying that to closures compared to fn pointers? Internally, we store a function pointer in ClosureSubsts to represent the signature, so adding bounded fn ptrs should extend very naturally to closures.

I wasn't aware of the early-bound / late-bound distinction for function regions until very recently - I forgot to add that to the pre-RFC. I think it might be useful to have, but I definitely need to think about what the syntax could be. On functions, you can also constrain a lifetime with a where clause to make it early-bound, but closures dont currently have where clauses.

Also, I'm having trouble coming up with a situation where it's useful to have an early-bound closure (as opposed to one with an inferred local region). For functions, early-bound lifetimes seem like more of a consequence of the limitations of higher-ranked regions, rather than a feature that's useful in its own right. Do you have one in mind?

In any case, this proposal should be compatible with adding in some kind of syntax for early bound lifetimes later on (since adding for<> requires that all lifetimes be unambiguously higher-ranked)

That's right - I have a short comment relating to that in the guide-level explanation

Thanks, I wasn't aware of that restriction.

Is there anything in the RFC that mentions the behavior of lifetimes in that position? There's been a lot of refactoring around impl Trait recently, and impl Trait inside a function body has always had a lot of bugs. I'd be cautious about relying on the current behavior with regard to lifetimes.

After some further investigation, it appears that closures currently never have early-bound regions:

Early-bound lifetimes are extracted from ast_generics: https://github.com/rust-lang/rust/blob/4f49627c6fe2a32d1fed6310466bb0e1c535c0c0/compiler/rustc_typeck/src/collect.rs#L1596

For closures, ast_generics will always be empty: https://github.com/rust-lang/rust/blob/4f49627c6fe2a32d1fed6310466bb0e1c535c0c0/compiler/rustc_typeck/src/collect.rs#L1576

My proposal won't change this, so I think we could just leave consideration of early-bound lifetimes on closures to a future proposal.

1 Like

If closures gained the ability to be constructable over a const or the like, when function pointers or Fn bounds/types couldn't, say. More generally, I'm trying to ensure we consider the possibility of a drift between how for<> works in different positions, which would be confusing. Maybe it's acceptable, or maybe there could be an alternative syntax.

No anticipation, but I wanted to make sure it had been considered since it had already been raised as a possibility elsewhere.

It does say "they also inherit any lifetime parameters in scope" (in contrast to naming the lifetime to bring it in scope). Though it's not clear to me if the scope refers to the function body context or the right-hand expression; if it's the latter, I don't think there's a difference in the resulting lifetimes. (You'll just end up with a impl Fn(&str) + 'anonymous.)


Before I reply to the question about early bounds, I want to highlight the possibility that we're talking about different things. Because you say:

But my understanding is based on the discussion here, where

  • Lifetimes are always late-bound or early-bound
  • Late-bound are specified per-use but early-bound are part of the type
    • That is, if the lifetime is part of the type, it can't be late-bound / must be early-bound
  • If a lifetime isn't part of an argument, it must be early-bound
    • As it must be part of the type so that the Fn implementation is constrained

So here for example:

fn foo(s: &str) -> impl Fn() + '_ {
    move || println!("{}", s)
}

fn bar<'a>(s: &'a str) -> impl Fn() -> &'a str {
    move || s
}

The closure types must have an early-bound lifetime. Conversely, if a closure has an inferred lifetime, that must be part of its type (or it would be 'static), and thus it must be early-bound.

Or more succinctly: Latebound is higher-ranked, early bound is not higher-ranked (though variance can make it look to be so -- e.g. where you had to use Cell to make a point).

It's possible I'm missing something about the implementation that wasn't covered in the linked description, though.

Given my understanding from above, a closure with an inferred local region is early-bound. And being able to force the early-bound covers the cases where being inferred as higher-ranked causes problems. It's a way to opt-out of being higher-ranked instead of relying on inference, which will be more important going forward if closure inference leans more in the higher-ranked direction.

Basically, reducing the need for extra syntax in the higher-ranked case increases the need for syntax in the non-higher-ranked case. If we can put our foot on the higher-ranked side of the see-saw, I think we should be able to put our foot on the not-higher-ranked side as well.

(Something is also tickling my mind about higher-ranked fn pointers and "just generic" fn pointers being distinct types; in the future, which you are may effect which implementation applies. But as closure types are unnameable, perhaps that won't directly matter to closures until we have some mechanisms to have multiple blanket implementations of a single trait (over Fn(T) and Fn(&T)).)

2 Likes

I would like to just nitpick that the proposed syntax for<'a> for "term-level" usage is new. Existing usages for function pointer types and HRTB are "type-level". It is probably more noticeable because I'm familiar with dependent-type programming languages, where a type of a function term is a pi type.

That being said, there is prior art to this "confusion" so it may be natural to programmers. For example, both the tuple construction and the tuple type use the same syntax (a, b) : (A, B).

Alternatively, fn keyword could be used, I think. Probably like let closure = fn<'a>|arg: &'a u8| { };

2 Likes

That's a good point. However, we currently parse and reject for<T> fn(T):

error: only lifetime parameters can be used in this context
 --> src/main.rs:2:16
  |
2 |     let a: for<T> fn(T);
  |                ^

If we later wanted to allow something like fn<const T: usize> || {} for closures, I think it would be conceptually straightforward for that syntax to be rejected in other situations, in the same way that we currently reject for<T> in all situations.

This is not the case (at least with how the terminology is used inside the compiler). Early-bound and late-bound refer to generic lifetimes (fn foo<'a>() {} or for<'a> fn() {}). However, inside a function body, we can have region variables, which are not really considered 'early-bound' or 'late-bound'. Closures are special in that they occur inside another body, and have their signatures determined by type inference. As a result, they can end up with a region variable in their signature, which is not normally possible in a signature.

See https://rustc-dev-guide.rust-lang.org/early-late-bound.html#early-bound-parameters for more discussion of how those terms are used inside the compiler, which is what I was referring to.

I would definitely like to see some kind of syntax for non-higher-ranked closure lifetimes in the future. However, I think for<'a> should always mean "a higher-ranked 'a lifetime", so accepting that syntax doesn't constrain our choices for indicating non-higher-ranked lifetime. With the future-compatibility restrictions in this RFC (denying type/lifetime elision when for<> is in use), I think we can defer making a decision about non-higher-ranked lifetimes without painting ourselves into a corner.

That was one of the reasons I decided to write this RFC (though I didn't end up mentioning it in the RFC itself). While I think it's not possible to directly observe that with closures at the moment (you need to insert an explicit cast to a function pointer where you would pick fn(T) or fn(&T)), it would definitely complicate the process of designing an 'ideal' closure region inference algorithm.

1 Like

Thanks for the link and summary! That raises some interesting (to me) questions about such closures, but I have no idea if they're relevant to this RFC, so I'll avoid derailing the conversation and dig into the distinction on my own at some point.

After reading that, I was going to suggest @pcpthm's approach (fn<...>||{}), and I still think it should be part of the Alternatives section. (I was skeptical of another future syntax on top of the for<>|| suggested here, if one syntax could handle both.)

Summary of my thought process around that approach

We have a syntax for non-higher-ranked functions already (though not a great one):

fn is_hr<'a>(_: &'a str) {}
fn not_hr<'a: 'a>(_: &'a str) {}
// `let _: fn(&str) = not_hr;` will fail due to the lack of HR
// (Also, `not_hr` is turbo-fishable while `is_hr` is not, so
//  I suspect this "meaning" is permanent.  It's also a workaround
//  for inference

We could have something similar for closures:

let closure_1 = fn<'a> |_: Cell<&'a u8>| {};
let closure_2 = fn<'a: 'a> |a: &'a str| fields.push(a);
// Intuition: Closures are like nameless functions to an extent.
//            Functions act this way w.r.t. lifetime parameters.
//
// Precedence: Generic params on implementations get attached to
// the `impl<...>` because implementations are nameless; there is
// no name to attach the parameters to.

Though I can also think of some downsides too (you can't have a fn<T> | ... | today).

But when refreshing my memory on related issues, I rediscovered this suggestion by petrochenkov, that for<> could be applied to fn declarations like so:

for<'late> fn f<'early1, 'early2>(
    a: &'late u8, 
    b: &'early1 u8,
    c: &/*'elided*/u8
) -> &'early2 u8 { ... }

That is,

If for is specified then all lifetimes defined in it are considered late-bound, and all lifetimes specified in generic parameters are considered early-bound.

Which would be a change for fn<'a /*no bound*/>(...){} requiring an edition. It would also clash with fn<'a> meaning "'a is higher-ranked" on closures.

They also point out that the suggested interpretation is already the case for aliases of fn pointers:

type F<'early1, 'early2> =
    for<'late> fn(&'late u8, &'early1 u8, &/*'elided*/u8) -> &'early2 u8;

And the same is true for the analogous impl Fn TAIT on unstable.

This aligns well with:

And has other benefits (no longer mixing early and late bound; if you put a lifetime in the fn name<...> list you can turbofish it). After working through that, I'm less skeptical of separate syntactical approaches for declaring higher-bounds and denying higher-bounds.

So perhaps it deserves a mention in the RFC.

1 Like

Do you have an example of when this would be useful? My understanding is that the early-bound/late-bound distinction is essentially just due to the limitations of the type system. If we allowed things like for<'a: 'b> fn(&'a u8, &'b u8) or for<'a> fn(&'a u8) where &'a u8: MyTrait (that is, 'higher-ranked' lifetime/trait bounds as part of a function pointer type), then we could just make all function lifetime parameters late-bound. Since we don't support those bounds, there is no way to convert such a function to a function pointer, since the function pointer would accept arguments that the underlying function does not.

The only thing I can think of is you wanted to specifically prevent the user from explicitly specifying a lifetime (e.g. I don't want to allow my_fn::<'static>(...)). However, you can't use the turbofish syntax with closure calls, so this doesn't apply here.

From my perspective, the two currently existing types of closure lifetimes (late-bound lifetimes which are higher-ranked, and local inferred regions which are non-higher-ranked) cover all use-cases for closures. I definitely think it would be useful to be able to explicitly indicate a non-higher-ranked lifetime for closure parameters in the future. However, these kinds of lifetimes are not parameters, so I think it would be quite misleading to use a syntax like fn<'a: 'a> for them.

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

RFC is up at Allow using `for<'a>` syntax when declaring closures by Aaron1011 · Pull Request #3216 · rust-lang/rfcs · GitHub. Thank you to everyone here for their feedback!

4 Likes

FWIW, I've written a PoC crate showcasing the ideas of that RFC, for those wanting to experiment with them in the interim:

1 Like

Lifetimes of closure parameters are typically related to the lifetimes of their captures, but I don't see any way to relate them via this proposal. How often would you really want truly unbounded parameter lifetimes anyway? While there are some cases where it could be useful, perhaps it would be better to declare them as local functions. In that case there would be no confusion about inferred and provided lifetimes, generic parameters, or any other feature that might be added to the functions.

Besides the general concern about an unaddressed issue (which is related to the impossibility of specifying specific lifetimes of the parameters), I think this RFC misses some case study from real-world code. How often patterns like that would be useful? Is there some common workaround, which would allow us to compare the benefits for specific code samples? I know that I hit an issue like that a few times in my code, but that was a very rare problem for me, and I'm not even sure that my issues would be solvable with this limited proposal.

As a more general objection, while I understand the desire to move in small steps and to carve out some unobjectionable exceptions as a way to move to the final goal, I also fear that it complicates the language for the end-users for too little cumulative benefit. It turns a hard simple boundary of the language possibilities into a fractal fuzzy limit which is hard to navigate. One never knows whether some issue is truly insolvable or just needs some obscure partially implemented language feature. One never knows whether some initial design will be implementable, or it will hit an indefinitely delayed unimplemented edge case at some faraway point and require an entirely different approach. Not without a lot of trial and error, that is.

In this case, with several very naturally expected and commonly required features being unavailable, I wonder whether it will be a net increase or decrease in the language complexity.

1 Like

This RFC was published as a PR on the RFCs repo. You should copy your concerns there:

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.