Implied lifetime of parameter to FnOnce trait

What does this mean — as in, which lifetime is inferred for A?

struct A<'a>(&'a ());
trait Foo {}

impl<F: FnOnce(A)> Foo for F {}

This compiles, but attempting to use it fails:

fn test() {
    fn use_foo<F: Foo>(_: F) {}
    use_foo(|_| {});
}
error: implementation of `FnOnce` is not general enough
  --> src/main.rs:15:5
   |
15 |     use_foo(|_| {});
   |     ^^^^^^^^^^^^^^^ implementation of `FnOnce` is not general enough
   |
   = note: closure with signature `fn(A<'2>)` must implement `FnOnce<(A<'1>,)>`, for any lifetime `'1`...
   = note: ...but it actually implements `FnOnce<(A<'2>,)>`, for some specific lifetime `'2`

error[E0308]: mismatched types
  --> src/main.rs:15:5
   |
15 |     use_foo(|_| {});
   |     ^^^^^^^^^^^^^^^ one type is more general than the other
   |
   = note: expected trait `for<'r> FnOnce<(A<'r>,)>`
              found trait `FnOnce<(A<'_>,)>`
note: this closure does not fulfill the lifetime requirements
  --> src/main.rs:15:13
   |
15 |     use_foo(|_| {});
   |             ^^^
note: the lifetime requirement is introduced here
  --> src/main.rs:14:19
   |
14 |     fn use_foo<F: Foo>(_: F) {}
   |                   ^^^

(BTW this error message is much improved in nightly over 1.62.0.)

So, the compiler tells us how to fix it: impl<'a, F: FnOnce(A<'a>)> Foo for F {}

But what does what we wrote above mean, and why is it even legal?

Aside: this is not legal:

trait Bar<B> {}

// error[E0106]: missing lifetime specifier
impl<F: Bar<A>> Foo for F {}
//          ^ expected named lifetime parameter
//
// help: consider introducing a named lifetime parameter
// impl<'a, F: Bar<A<'a>>> Foo for F {}

We get a very helpful error at the impl site here, so why not above?

Well, it may be since lifetimes can be inferred in functions:

// legal: lifetime 
fn use_A(_: A) {}

fn test2() {
    let a = ();
    let a = A(&a);
    // lifetime is inferred at the call site:
    use_A(a);
}

This implies that use_A has a hidden lifetime parameter, effectively:

fn use_A<'a>(_: A<'a>) {}

So, going back to our initial impl, what does it mean and which lifetime is inferred?

impl<F: FnOnce(A)> Foo for F {}

Going by the error we hit with use_foo, it does not mean what we might expect, that F: FnOnce(A<'a>) for all 'a. The message seems to indicate that 'a is considered an existential. Is there precedence for using existentials in impls? (I know existentials may be used for the opposite: to prove that two impls conflict with some parametrization.) Or is the mentioned '2 actually bound to something, for example the (unnamable) lifetime of F?

The point of this post: should impls with implied existential lifetimes like above be deprecated? They are confusing and do not appear to have a valid use-case. The use-case is valid; see below.

This question might be more fitting in the users forum?

Skip to the bold bit at the end.

I’m focusing on answering this last question, since you seem to be asking about language design, and I‘m apparently supposed to ignore that 99% of your post is about



So the answer is that your premise that “They … do not appear to have a valid use-case” seems wrong. There is a valid use-case for this impl of yours:

struct A<'a>(&'a ());
trait Foo {}

impl<F: FnOnce(A)> Foo for F {}

fn test() {
    fn use_foo<F: Foo>(_: F) {}
    use_foo(|_: A| {});
}

(compiles successfully in the playground)

the error you’re getting is just a limitation of how inference of closure types works (or doesn’t work very well) when the trait bound of the function you’re passing the closure to is not directly using an Fn… trait but a different trait that’s generically implemented for some F: Fn….

1 Like

1 important reason not to do this: It is likely used a lot and so would likely cause some pretty heavy breakage in the ecosystem.

While reading the code I figured as much, and the error locations do hint at it. But the actual error message in OP is pretty misleading. Perhaps the improvement in 1.62 OP hinted at corrects this?

So what you're saying is that the closure should like is this (if for<'a> were allowed in this context):

use_foo(for<'a> |_: A<'a>| {});

How does one use this? Answered above: with _: A.

This is the improved error message. Rustc 1.62.0 only misses the first part (error: implementation of FnOnce is not general enough).

It sounds iin this case like the addition is misleading.

I feel like the error message accurately describes what’s going on. Closures with completely inferred types tend to not be generic over lifetimes, which is why |_| {} fails. Once you add an explicit type annotation containing an elided lifetime, they tend to become generic, which is why |_: A| {} works.

Implicit elided lifetimes are confusing in general IMO, which is why you should probably write every A as A<'_>.

Read more about lifetime elision (also here) and HRTBs if you want to make sense about the meaning of FnOnce(A<'_>) or why impl<F: Bar<A<'_>>> is not allowed, or ask in the users forum for some more extensive explanation.

impl<F: Fn(A)> Foo for F {}

Lets try answering this using the nomicon linked above:

For impl headers, all types are input.

Each elided lifetime in input position becomes a distinct lifetime parameter.

So, the elided lifetime becomes distinct. But the rest of the page only talks about resolving elided output lifetimes, which is not relevant here.

The HRTB article is more useful however, and points us to the answer:

impl<F> Foo for F
where
    for<'a> F: Fn(A<'a>),
{}

Thus, the lifetime is not an existential as implied by the error message (on nightly):

= note: ...but it actually implements FnOnce<(A<'2>,)>, for some specific lifetime '2

(Perhaps what is omitted here is: for<'2>)

Even if it is accurate, the error message doesn't actually help the reader to fix the issue unless that reader knows about a particularly dark corner of lifetime elision rules. For example, I knew about the lifetime elision rules, but I didn't know about this corner case.

Once you do know about that it'll help you fix the issue, that's true. But how many people that will actually run into this will know that? That's why I call it an unhelpful error message.

3 Likes

This is a really good point. If the error message was replaced with a typical type annotations needed message, the error would be a lot easier to resolve.

Maybe you’re reading the message the wrong way around?

error: implementation of `FnOnce` is not general enough
 --> src/lib.rs:8:5
  |
8 |     use_foo(|_| {});
  |     ^^^^^^^^^^^^^^^ implementation of `FnOnce` is not general enough
  |
  = note: closure with signature `fn(A<'2>)` must implement `FnOnce<(A<'1>,)>`, for any lifetime `'1`...
  = note: ...but it actually implements `FnOnce<(A<'2>,)>`, for some specific lifetime `'2`

This says that the |_| {} closure implements `FnOnce<(A<'2>,)>`, for some specific lifetime `'2` , but calling use_foo requires that it must implement `FnOnce<(A<'1>,)>`, for any lifetime `'1` .

I feel like this could be resolved with a hint:

help: consider adding a type annotation:
   |
 8 |     use_foo(|_: A<_>| {});
   |               ~~~+++

(I don't have much familiarity with the compiler — would someone else be willing to make a PR regarding this?)

(if you make an issue, @ekuber and @compiler-errors were tongue-in-cheek complaining about a lack of new diagnostics issues to fix...)

3 Likes

Only one thing to add: I thought we had a lint suggestion to use <'_> when an elided lifetime is present on a type. It's presence would at least make the issue easier to spot, but I couldn't get the compiler to suggest changing A to A<'_> at all...

1 Like

Done: Diag: closure with implied type lacks generic lifetime · Issue #99645 · rust-lang/rust · GitHub

1 Like

elided_lifetimes_in_paths

Still allow-by-default. At least it’s part of rust_2018_idioms.

Why is that lint group not on by default yet‽

Also, #![warn(rust_2018_idioms)] reads wrong :upside_down_face:

2 Likes