Help understanding a detail of lifetimes

One thing to be aware of is that every function ("function items" like fn foo) has it's own (zero-sized, not a pointer) type. The type is not nameable. Each closure also has an unnameable type (the size depends on what it captures).

Function item types can be cast to function pointers, but are not themselves function pointers.

foo doesn't implement FnOnce() -> &'a str for any 'a,[1] it implements FnOnce() -> &'static str.

Your closure can implement FnOnce() -> &'a str for any 'a by using covariance of the obtained &'static within the closure body. It doesn't have to return &'static str. And if you make it do so...

b.unwrap_or_else(|| -> &'static str { foo() })
// error[E0521]: borrowed data escapes outside of function

These types that return &'static str are like a struct with no generic parameters. So they don't have supertypes you can implicitly coerce to, shortening the lifetime. Trait implementations themselves don't have variance, might be another way to put it.

Compare and contrast with bar here:

fn bar<'a>() -> &'a str { "bar" }

bar is like a struct with a generic lifetime parameter. When you pass the bar, you really pass bar::<'_> where a suitable lifetime can be inferred.

Alternatively you can cast foo to be a function pointer.

b.unwrap_or_else(foo as fn() -> &'a str)

// These also work:
b.unwrap_or_else(foo as fn() -> &'static str)

The difference here is that fn() -> &'_ str, like bar, is acting like a struct with a generic lifetime parameter, which is covariant in that lifetime. So this function pointer (and bar's type) does have a supertype that it can implicitly coerce to.

How exactly function item types and closures implement the Fn traits is pretty subtle. Here's another recent post about it in a more complicated setting.

  1. that's actually a bound that's not even recognized as well formed ↩︎