3

Allow using `for<'a>` syntax when declaring closures by Aaron1011 · Pull R...

 2 years ago
source link: https://github.com/rust-lang/rfcs/pull/3216
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Copy link

Member

Aaron1011 commented 10 days ago

edited by estebank

Rendered

Allow declaring closures using the for<'a> syntax:

let closure = for<'a> |val: &'a u8| println!("Val: {:?}", val);
closure(&25);

This guarantees that the closure will use a higher-ranked lifetime, regardless of how the closure is used in the rest of the function.

This went through a pre-RFC at https://internals.rust-lang.org/t/pre-rfc-allow-for-a-syntax-with-closures-for-explicit-higher-ranked-lifetimes/15888. Thank you to everyone who provided feedback!

let short_cell: Cell<&u8> = Cell::new(&val);

closure(short_cell);

}

```

FWIW, there is no need for invariance to show the problems of an inferred / non-higher-order lifetime parameter:

fn main ()
{
    let closure = |s| {
        let _: &'_ i32 = s; // type-annotations outside the param list don't help.
    };
    {
        let local = 42;
        closure(&local);
    }
    {
        let local = 42;
        closure(&local);
    }
}

fails as well.

or even shorter:

let closure = |_| ();
closure(&i32::default());
closure(&i32::default());
Aaron1011 and eddyb reacted with thumbs up emoji

# 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) -> &'a u8`

Super tiny nit: there is currently no mention of for<…> syntax combined with move (and/or async); so maybe spell out super-explicitly the fact for<…> would be followed by our current closure expressions, with at least one example mentioning for<…> move |…| slightly_smiling_face

lebensterben, clarfonthey, and estebank reacted with thumbs up emoji

Copy link

Contributor

Diggsey commented 10 days ago

Would this allow the following code to be uncommented:

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=465408ec84cee8e2736c0ac6cc46bade

This has been a long time limitation of async functions, so it would be great if it allowed that restriction to be lifted, but the RFC isn't clear whether this is expected to work, since it relies on the returned future also being generic over the lifetime.

Copy link

Member

Author

Aaron1011 commented 10 days ago

@Diggsey: I think that issue would be unaffected by this RFC, since there isn't a way to explicitly specify the desugared return type (let along lifetime) of an async move closure.

Copy link

Contributor

Diggsey commented 10 days ago

@Aaron1011 I'm not sure I follow? You can replace example in my code with this definition:

fn example<'a>(arg: &'a i32) -> impl Future + 'a {
    async move {
        *arg == 42
    }
}

And it still compiles - there was no need to specify a "return type" for async move, it just gets inferred.

Copy link

Member

Author

Aaron1011 commented 10 days ago

@Diggsey I think the root of that issue is the inability to write a higher-order async move closure:

fn main() {
    let _ = |arg: &i32| async move {
        arg;
    };
}

produces:

error: lifetime may not live long enough
 --> src/main.rs:2:25
  |
2 |       let _ = |arg: &i32| async move {
  |  ___________________-___-_^
  | |                   |   |
  | |                   |   return type of closure `impl Future<Output = [async output]>` contains a lifetime `'2`
  | |                   let's call the lifetime of this reference `'1`
3 | |         arg;
4 | |     };
  | |_____^ returning this value requires that `'1` must outlive `'2`

Copy link

Contributor

Diggsey commented 10 days ago

Isn't my last example just as "higher-order" as the closure example?

Copy link

Member

Author

Aaron1011 commented 10 days ago

Do you mean my closure example, or something from the RFC itself?

I think the problem you're running into has to do with the way that we desugar an async move closure. When you write async fn bar<'a>(val: &'a) {}, then the desugared impl Future return type ends up 'capturing' the 'a lifetime from the parameter. However, that doesn't seem to be happening in the closure example, leading to the error.

Copy link

Contributor

Diggsey commented 10 days ago

edited

I mean:

fn example<'a>(arg: &'a i32) -> impl Future + 'a {
    async move {
        *arg == 42
    }
}

(works)

|arg: &i32| async move {
    *arg == 42
};

(does not work, but might with for<'a>)

When you write async fn bar<'a>(val: &'a) {}, then the desugared impl Future return type ends up 'capturing' the 'a lifetime from the parameter.

But my example doesn't use async fn

Copy link

Member

Author

Aaron1011 commented 10 days ago

But my example doesn't use async fn

I mean that the code should pretty much equivalent to the async fn I mentioned, so I think it might just be a bug in how the desugaring is being performed.

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.

Previously, we only allowed thw `for<>` syntax in a 'type' position: function pointers (`for<'a> fn(&'a u8)`) and higher-ranked trait bounds (`where for<'a> T: MyTrait<'a>`). This RFC requires supporting the `for<>` syntax in an 'expression' position as well (`for<'a> |&'a u8| { ... }`). While there should be no ambiguity in parsing, crates that handle parsing Rust syntax (e.g. `syn`) will need to be updated to support this.

There is a parsing ambiguity "generics vs qualified path" (same as in impl <A> :: B) due to qpath patterns in for loops, but thankfully we have some future proofing for this case:

https://github.com/rust-lang/rust/blob/e012a191d768adeda1ee36a99ef8b92d51920154/compiler/rustc_parse/src/parser/expr.rs#L1257-L1263

Aaron1011 reacted with thumbs up emoji

* 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 interpretation 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).

FWIW, I think the second alternative is preferable, since it would also be consistent with this my suggestion rust-lang/rust#42868 (comment) for supporting for<...> parameters on function items.

Copy link

Member

Author

@Aaron1011 Aaron1011 10 days ago

The main issue with this approach is that it would require us to either:

  1. Make a breaking change to closure inference, since |val: &i32| is (usually) higher-ranked
  2. Change the behavior of &T (with an elided lifetime) depending on whether or not a for<> binder is present, which would be inconsistent with function pointers (for<'a> fn(&'a u8, &u8) is equivalent to for<'a, 'b> fn(&'a u8, &'b u8))

In the #42868 suggestion, elided function argument lifetimes are still late-bound, like they are today, correct? If there was a way to indicate non-higher-ranked lifetimes on closures, that would open up a third alternative: Make elided lifetimes in the closure argument list higher-ranked, like function args today. (Also discussed in the pre-rfc thread. This RFC is future-compatible with that route as far as I can tell.)

Copy link

Member

Author

Aaron1011 commented 10 days ago

@Diggsey On further investigation, I believe that this RFC is actually related to your issue. Currently, there's no easy way of getting a closure of the form for<'a> |input: &'a T| -> &'a T| (with a matching higher-ranked lifetime in the input and output). For example, the following code:

fn main() {
    let _ = |val: &i32| -> &i32 { val };
}

produces the following error:

error: lifetime may not live long enough
 --> src/main.rs:2:35
  |
2 |     let _ = |val: &i32| -> &i32 { val };
  |                   -        -      ^^^ returning this value requires that `'1` must outlive `'2`
  |                   |        |
  |                   |        let's call the lifetime of this reference `'2`
  |                   let's call the lifetime of this reference `'1`

In order to get the desired signature, you need to pass the closure to a function expecting an FnOnce with the desired signature. With this RFC, you can directly specify the desired lifetime with for<'a>.

However, this unfortunately isn't enough to fix your case - you can't write impl Future + 'a in the return type. You would need the ability to write impl Trait in a closure return type, or the compiler would need to infer the '+a for you. However, using the FnOnce trick, you can get it to compile by introducing an additional trait:

trait MyTrait {}

impl<T> MyTrait for T {}

fn constrain<T, F: FnOnce(&T) -> Box<dyn MyTrait + '_>>(val: F) -> F { val }

fn main() {
    constrain(|arg: &i32| {
        Box::new(async move {
            *arg == 42;
        })
    });
}

Copy link

Contributor

WaffleLapkin commented 10 days ago

Could # Future possibilities also mention bounds? Like for<'a, 'b: 'a> |...| {...} and similar stuff? Or is it way out of scope since for<'a, 'b: 'a> fn(&'a u8, &'b u8) type isn't allowed either?

Copy link

Member

Author

Aaron1011 commented 10 days ago

@WaffleLapkin Since we currently don't have higher-ranked bounds for function pointers, I would personally consider that out of scope.

Copy link

cynecx commented 8 days ago

@Diggsey, @Aaron1011 Is this rust-lang/rust#70263 related to the issue you are talking about?

Copy link

Contributor

@nikomatsakis nikomatsakis left a comment

I would like to improve inference, but I also believe we need some explicit syntax, and this is the obvious one. Generally +1 from me.

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.

Copy link

Member

@nrc nrc 7 days ago

This seems relatively easy to design up front (i.e., follow the rules for lifetime-generic functions with elided lifetimes). Unless there are difficult questions to be answered, it seems better to discuss this at the design stage than to kick it down the road and add another rough edge.

follow the rules for lifetime-generic functions with elided lifetimes

Well, the issue then would be that while such an approach would favor fully higher-order signatures, and we'd also have the question of the hybrid ones.

let ft = 42_u8;
// The outer lifetime parameter in `elem` can be higher-order, but not the inner one.
let f = for<'s> |x: &'s str, y: &'s str, elem: &mut &u8| -> &'s str {
    *elem = &ft;
    if x.len() > y.len() { x } else { y }
};

So I don't personally think it is that easy; there is currently no way to favor some use cases without hindering others. So the best thing, right now, would be to "equally hinder all the ambiguous ones", by conservatively denying them, and see what is done afterwards.

FWIW, some kind designator for 'in-ferred lifetimes, such as 'in, or, say, '? could be added, I think:

let ft = 42_u8;
let f = for<'s> |x: &'s str, y: &'s str, elem: &mut &'? u8| -> &'s str {
    *elem = &ft;
    if x.len() > y.len() { x } else { y }
};
  • (Or '_?). And '* / '_* for a disambiguated higher-order elided lifetime parameter?

But I also agree that some of these "left for the future" questions can end up taking years to be resolved, for something that doesn't warrant that much thinking, just because the primitive feature already lifted most of the usability pressure off it (I'm, for instance, thinking of turbofish not having been usable for APIT functions).

Aaron1011 and WaffleLapkin reacted with thumbs up emoji

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK