When does lifetime coercion happen?

Playground

use once_cell::sync::Lazy;

fn default() -> &'static Vec<i64> {
    static EXAMPLE: Lazy<Vec<i64>> = Lazy::new(Vec::new);
    &EXAMPLE
}

fn main() {
    let v: Option<Vec<i64>> = None;
    
    // (a) Why does this work
    let borrow = v.as_ref().unwrap_or_else(|| default());
    
    // (b) But not this?
    let borrow = v.as_ref().unwrap_or_else(default);
}

This is minimal example of a real life case that I had. What is the difference between these two cases. So far my understanding is that in (a) the 'static lifetime coerces to the lifetime of v but in (b) it doesn't and requires borrow to be a 'static lifetime. What are the rules for this, when will the coercion happen or not? Or is this because the lifetime of borrow is inferred differently in each case?

Here are two relevant references:

Here, unwrap_or_else<F> needs F: FnOnce() -> T. With the closure in the first case, it can infer that T = &'_ Vec<i64> with an anonymous local lifetime, and the closure can satisfy that by coercing its &'static Vec<i64>. But with the direct function in the second case, F is fixed as the function's direct type with T = &'static Vec<i64>, and that trickles back to the lifetime requirements on the rest of the expression.

You can use coercion and variance another way, by forcing it to a generic function pointer:

    let borrow = v.as_ref().unwrap_or_else::<fn() -> _>(default);

That fn() -> _ still isn't naming the lifetime, but the inference works out. So default coerces to a function pointer, and fn() -> T is covariant in T, so it can also subtype the lifetime from 'static. This isn't totally satisfying, because a function pointer adds indirection, though it may get optimized back to a direct call anyway.

I think I still haven't explained it very well, but I hope those pieces get you on the right track!

2 Likes

I think the question you should be asking is not "when" but "where".

In (a) the "coercion site" (actually lifetime subtyping is not exactly a coercion; however, the analogy is sufficient to describe this case) is inside the closure, where it tries to return a value of type &'static _ where the required return type (inferred from the context) is &'a _. In (b) there is no coercion site because unwrap_or_else takes a generic F, so F is inferred to be exactly the type of default, which does not implement FnOnce() -> &'a _ (instead it implements FnOnce() -> &'static _).

You can make functions like default return a parametric lifetime, to avoid problems like this:

fn default<'a>() -> &'a Vec<i64> {...}
2 Likes

Taking the two answers together, we can see that while the return type of a function pointer can be covariant, that of unnameable function types are (at least currently) invariant.

2 Likes

Thanks all, this makes a lot more sense now.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.