Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Annotating higher-ranked lifetimes on closures is arduous #58052

Open
pnkfelix opened this issue Feb 1, 2019 · 6 comments
Open

Annotating higher-ranked lifetimes on closures is arduous #58052

pnkfelix opened this issue Feb 1, 2019 · 6 comments
Labels
A-closures Area: Closures (`|…| { … }`) A-lifetimes Area: Lifetimes / regions T-lang Relevant to the language team, which will review and decide on the PR/issue.

Comments

@pnkfelix
Copy link
Member

pnkfelix commented Feb 1, 2019

Executive summary: If you want to make a closure returning a higher-ranked lifetime, you need to use a helper like fn annotate<T,F>(f: F) -> F where F: Fn(&T) -> &T { f }. We could probably do better.


Spawned off of #22557 (comment) (and possibly a duplicate of #22340 )

Consider this code (play):

fn main() {
    let f = |x: &i32| x;
    let i = &3;
    let j = f(i);
}

It doesn't compile. And its diagnostic is pretty hard to understand.

You can see an explanation from @nikomatsakis about why it doesn't compile here: #22557 (comment)

With #![feature(nll)], it still doesn't compile; the diagnostic is slightly better:

error: lifetime may not live long enough
 --> src/main.rs:4:23
  |
4 |     let f = |x: &i32| x;
  |                 -   - ^ returning this value requires that `'1` must outlive `'2`
  |                 |   |
  |                 |   return type of closure is &'2 i32
  |                 let's call the lifetime of this reference `'1`

The aforementioned explanation claims that adding a return type will get it to compile. But when I tried that, it did not work, both with and without #![feature(nll)] (play, which includes NLL since I like its diagnostic here better):

fn main() {
    let f = |x: &i32| -> &i32 { x };
    let i = &3;
    let j = f(i);
}

yields (and we'll leave #58053 in its own bug):

error: lifetime may not live long enough
 --> src/main.rs:4:33
  |
4 |     let f = |x: &i32| -> &i32 { x };
  |                 -           -   ^ returning this value requires that `'1` must outlive `'2`
  |                 |           |
  |                 |           return type of closure is &'2 i32
  |                 let's call the lifetime of this reference `'1`

So what gives? Well, I think when @nikomatsakis claimed that an explicit return type would work, they were assuming that an explicit return type would cause lifetime elision rules to apply such that the same lifetime would be provided for the input and output reference-types on f. But as we saw in #56537, lifetime elision rules do not apply to closure return type annotations.


So what we want is to say that we have a lifetime parametric closure, with a type something like for<'a> Fn(&'a i32) -> &'a i32. But no, that's not a type, its a trait bound, so this does not work either (play):

fn main() {
    let f: for<'a> Fn(&'a i32) -> &'a i32 = |x| x;
    let i = &3;
    let j = f(i);
}

yields:

error[E0308]: mismatched types
 --> src/main.rs:4:45
  |
4 |     let f: for<'a> Fn(&'a i32) -> &'a i32 = |x| x;
  |                                             ^^^^^ expected trait std::ops::Fn, found closure
  |
  = note: expected type `dyn for<'a> std::ops::Fn(&'a i32) -> &'a i32`
             found type `[closure@src/main.rs:4:45: 4:50]`

error[E0277]: the size for values of type `dyn for<'a> std::ops::Fn(&'a i32) -> &'a i32` cannot be known at compilation time
 --> src/main.rs:4:9
  |
4 |     let f: for<'a> Fn(&'a i32) -> &'a i32 = |x| x;
  |         ^ doesn't have a size known at compile-time
  |
  = help: the trait `std::marker::Sized` is not implemented for `dyn for<'a> std::ops::Fn(&'a i32) -> &'a i32`
  = note: to learn more, visit <https://doc.rust-lang.org/book/ch19-04-advanced-types.html#dynamically-sized-types-and-the-sized-trait>
  = note: all local variables must have a statically known size
  = help: unsized locals are gated as an unstable feature

An approach that does work is to feed in the trait bound explicitly via a helper function, like this (play):

fn annotate<T,F>(f: F) -> F where F: Fn(&T) -> &T { f }

fn main() {
    let f = annotate(|x| x);
    let i = &3;
    let j = f(i);
    assert_eq!(*j, 3);
}

But that seems like a pretty arduous way to encode a relatively simple pattern. If you look at #22340 (comment), you can see others have suggested syntaxes like for <'a> |x: &'a i32| -> &'a i32 { x }, (where the for <'a> CLOSURE_EXPR is the new interesting expression form), which would be more convenient for addressing cases like ths.

@pnkfelix pnkfelix changed the title Annotating higher-ranked lifetimes on closures is arduous. Annotating higher-ranked lifetimes on closures is arduous Feb 1, 2019
@estebank estebank added A-lifetimes Area: Lifetimes / regions A-closures Area: Closures (`|…| { … }`) T-lang Relevant to the language team, which will review and decide on the PR/issue. labels Feb 1, 2019
@Centril
Copy link
Contributor

Centril commented Feb 2, 2019

I think that:

fn main() {
    let f = |x: &i32| x;
    let f = |x: &i32| -> &i32 { x };
    let f: impl for<'a> Fn(&'a i32) -> &'a i32 = |x| x;
    let f = for<'a> |x: &'a i32| -> &'a i32 { x };

    let i = &3;
    let j = f(i);
}

should all be equivalent (well ~ the opaque nature of impl ...) and Just Work.

ISTM that for<'a> isn't properly inferred when you let bind a closure unlike when you pass it immediately to a function. I see no good justification for that right now.

As for introducing for<'a> |x: &'a| ... it's on my TODO list wrt. for<T: Debug> and generic closures in general. I've been working on a draft but haven't messed with it for a while.

@fredericvauchelles
Copy link

👍 It will be very nice to have higher-ranked lifetimes for closure. (Very nice to write easily schedulable tasks)

@tema3210
Copy link

tema3210 commented May 22, 2021

Is there any plans for smth. like for<'a,'b: 'a> impl Fn(&'a T)->U? (syntax is really bad, but still) Because we don't have any way to say "for all lifetimes that are shorter (or longer) then 'x ..." i.e. bounded quantification for lifetimes?.

@nikomatsakis
Copy link
Contributor

We don't presently have clear plans to support this syntax, although we are actively at work on extending rustc internally to support that sort of thing.

@jinohkang-theori
Copy link
Contributor

#98705 implemented the closure lifetime binder syntax. However, the syntax requires the user to fully specify the type signature, so I'd argue that using higher-ranked lifetimes on closures are still tedious.

@theemathas
Copy link
Contributor

theemathas commented Jun 9, 2024

Another use case where one runs into this issue:

fn print_ref(value: &i32) {
    println!("{value}");
}

fn main() {
    let f = |x| print_ref(x);
    {
        let y = 1;
        f(&y);
    }
    {
        let z = 2;
        f(&z);
    }
}
Compile error
error[E0597]: `y` does not live long enough
  --> src/main.rs:9:11
   |
8  |         let y = 1;
   |             - binding `y` declared here
9  |         f(&y);
   |           ^^ borrowed value does not live long enough
10 |     }
   |     - `y` dropped here while still borrowed
...
13 |         f(&z);
   |         - borrow later used here

For more information about this error, try `rustc --explain E0597`.
error: could not compile `playground` (bin "playground") due to 1 previous error

However, for some reason, annotating the type with |x: &i32| makes the code compile.

The workaround that compiles
fn print_ref(value: &i32) {
    println!("{value}");
}

fn main() {
    let f = |x: &i32| print_ref(x);
    {
        let y = 1;
        f(&y);
    }
    {
        let z = 2;
        f(&z);
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-closures Area: Closures (`|…| { … }`) A-lifetimes Area: Lifetimes / regions T-lang Relevant to the language team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests

8 participants