Is `'a` early bound or late bound in impl Fn(&'a str)?

Hi,everyone. I'm trying to learn concept about early bound/late bound by reading this: Early vs Late bound parameters - Rust Compiler Development Guide. However, this example makes me confused:

fn say_some<'a>(name: String) -> impl Fn(&'a str) + 'static {
    move |text| println!("{name} syas: {text}")
}

fn main() {
    let func = say_some("Blackbird".into());
    func(&String::from("hello"));
    func(&String::from("world"));

    let func = say_some::<'static>("Blackbird".into());
    func("hello world");
}

Is 'a in say_some early bound or late bound? If it's early bound, why can func receive two reference with different lifetimes?(Monomorphization 'a can't satisfy two temporary lifetiems) If it's late bound, why can I use turbofish syntax to specify 'static when calling say_some?

Maybe the first say_some desugar to fn say_some(name: String) -> impl for<'a> Fn(&'a str) with HRTBs?

It's early bound, but ... I believe this is the situation: The say_some method returns an object that satisfies:

  1. It implements Fn(&'a str) for the specific lifetime 'a.
  2. The underlying type is 'static, that is, the underlying type has no lifetime annotations shorter than 'static.

There are only two possible ways to implement Fn(&'a str):

  1. Implement it for all lifetimes for<'b> Fn(&'b str).
  2. The type is annotated with 'a and implements it for 'a.

Since the +'static constraint rules out the latter case, it must fall into the former case. Thus, it implements not only Fn(&'a str), but it also implements for<'b> Fn(&'b str).

If you return impl Fn(&'a str) without the 'static, you get the expected errors.

1 Like

Interesting... If I remove the + 'static in say_some signature, it would be equal to impl Fn(&'a str) + use<'a>, right? Then 'a must outlive the scoop of func. So I get errors "temporary value dropped while borrowed". Does that also mean the variable func can't receive references with different lifetime?

I can't explain everything about this snippet but I can add some observations.

'a is early bound in say_some because it's not constrained by an input lifetime (it shows up, but only in the return type). If it was late-bound, you couldn't turbofish it. But that's a statement about the properties of the say_some function item type, and not about the returned opaque impl type, which is what you're interested in:

Is ’a early bound or late bound in impl Fn(&’a str)?

I believe what you're really asking is, can the returned opaque type accept different lifetimes in its input parameters? And the answer is no, it can only take one lifetime due to a few factors:

  • You don't get to assume opaques have implemented more than their bounds and their auto-traits, and the bound here has a single lifetime in the parameter
  • Bounds don't consider variance, so the contravariance you may expect from fn(&'a str) doesn't apply in this case

So the answer to this is no, which you can demonstrate by trying to coerce to higher-ranked dyn type.

Adding use<'a> doesn't do anything because 'a was already mentioned in the bounds -- it was already capturing 'a.

+ 'static doesn't mean "I don't capture 'a", it means "I meet a 'static bound". It's subtle, but here's an example where the lifetime must be captured even though the returned type meets a 'static bound, because the definition of the underlying type depends on the lifetime from a type system perspective.

I can't tell what you're thinking here.


Now I'll concentrate on the code.

There are two different errors when you remove the 'static bound.

8 |     func(&String::from("world"));
  |     ---- borrow later used here
12 | }
   | - borrow might be used here, when `func` is dropped and runs the destructor for type `impl Fn(&str)`

The latter is because you generally can't assume opaques do not have destructors that examine their captures. And indeed, with this signature, you could make a closure that does so.

The borrow checking around drops understands that Copy types don't have destructors, so we can remove that from the snippet with some leaking. However, the first of the two errors remains.

When the 'static bound is present, both errors go away. I can see why it would fix the drop related errors: If the type is 'static, there's no way for it to soundly store borrows. But I'm afraid I don't have an explanation for the first error going away. Erasing the opaque revives the error, even.

Callers aren't supposed to be allowed to take advantage of that. And the failure to coerce to a higher-ranked dyn Fn(&str) shows that this is at least being partially enforced in the example. But I can't explain why it does work; perhaps it is somehow partially leaking to the caller?

2 Likes

Thanks for your reply. Really.

Well, when I remove + 'static, the compiler will implicitly add + 'a bound, right? Would that be different with adding + use<'a> manually? Then the caller only knows return type satisfy T: 'a, so compiler will infer 'a at least as long as use of func in order to avoid dangling pointer. That is why the code below has an error:

fn say_some<'a>(name: String) -> impl Fn(&'a str) + 'a {
    move |text| println!("{name} syas: {text}")
}

fn main() {
    let func = say_some("Blackbird".into());
    {
        let string: String = "Talk is cheap.".into();
        let r = &string; // error: string doesn't live long enough
        func(r);
    }
}

Compiler will restrict you from using func outside 'a for safety. So moving the declaration of string to first line will fix the error.

I will continue to sleep. Chatting tomorrow ~

1 Like

In edition 2024, there's implicitly a + use<'a> unless you write your own + use<..> bound. That's true whether or not there's a + 'static. Removing 'static doesn't add a + 'a bound. That's an outlives bound, which is different from mentioning a lifetime in use<..> (or in a trait parameter). Let me whip up a quick example...

Here we go. If there was an implicit + 'a bound, the first call would fail, and if there was an implicit + 'b bound, the second call would fail. An outlives bound isn't the proper mechanism for the example. The return type involves both lifetimes, but doesn't necessarily outlive either. (The underlying return type is valid for the intersection of the lifetimes, not for one or the other or the union of the lifetimes.)

In edition 2021, outside of traits, -> impl Trait only captures lifetimes that are mentioned in the return. And before precise capturing (+ use<..>), the easiest way to do that was to add + 'a. The compiler even recommended it. But this adds an outlives bound requirement to the return type, which always isn't desired. I used two lifetimes in my example, but it was also a problem when there was a generic type T involved, as those are always captured -- so you were adding a T: 'a bound which was somewhat problematic.

So we needed a way to mention lifetimes without adding the bound. That -- especially in combination with the capturing rules changing in edition 2024[1] -- is what precise capturing is about. (There were ways to accomplish that before precise capturing, but they were considered hacky.)

Here's the RFC for precise capturing, but I actually think RFC 3498[2] has a better explanation of how captures work.


This is probably related, as this works. And perhaps type erasure does not because dyn Fn(&'a str) + 'static does not meet a 'static bound. Thinking about that a little more, the fact that OpaqueReturn: 'static means that there are no non-'static lifetimes in the definition of OpaqueReturn, even though it may capture a lifetime for other reasons. Perhaps that's the key distinction.

I'm reminded of a conversation, let me see if I can dig it up... this is the analysis I'm reminded of, and there is some more (learning as we go) conversation in this thread.


OK, after mulling a bit more, I believe I understand. Let me expand the code so it's easier to annotate.

fn main() {
    // Let's say the lifetime in the function call is `'s`
    let func = say_some("Blackbird".into());    // L1
    {                                           // L2
        let x = String::from("hello");          // L3
        // Let's call this lifetime `'x`        // L4
        func(&x);                               // L5
    }                                           // L6
    {                                           // L7
        let y = String::from("world");          // L8
        // Let's call this lifetime `'y`        // L9
        func(&y);                               // L10
    }                                           // L11
}                                               // L12

func can only take &'s str as per the opaque type bounds. Due to the calls on L5 and L10, we must have 'x: 's and 'y: 's. Yet, with the 'static bound on the opaque, this compiles. Isn't that a problem -- doesn't that mean x is borrowed on L6, when it gets destructed?

Not necessarily. Within a function body, lifetimes and borrows can have discontinuities. Consider this part of the NLL RFC for some discussion; you may want to skip to the conclusion and compare with the RFC example. Lifetimes and borrows are determined largely by data flow analysis; a discontinuity in a lifetime can cause a borrow to stop propagating forward.

With the 'static bound on the opaque, the type of the opaque cannot contain 's, even though it captured it. That's a well-formedness assumption baked into the language. Let's say the borrow checker makes use of that conclusion in this case. Then in terms of the NLL RFC, 's is not "directly alive", as it doesn't appear in the type of any variables. It only shows up in the subtyping constraints (('x: 's) @ L5, ('y: 's) @ L10). Quoting the RFC:

The meaning of a constraint like ('a: 'b) @ P is that, starting from the point P, the lifetime 'a must include all points in 'b that are reachable from the point P. The implementation does a depth-first search starting from P; the search stops if we exit the lifetime 'b.

If the type of the opaque cannot contain 's, then 's doesn't exist on L6 or L11, and 'x and 'y don't expand ("the search stops if we exit the lifetime").


Without the 'static bound, it is possible that the type of the opaque does include/contain the captured lifetime. Indeed, I already showed a playground where this happens without changing the function signature further. In that case, even if we work around the destructor borrow checking issues, every use of func keeps 's "directly alive". That means 's has liveness constraints on all lines between the uses on L5 and L10.[3] In combination with 'x: 's, that means x remains borrowed on L6 -- but that conflicts with going out of scope.

Here's a playground for that scenario. If you remove Copy, there's an additional error as the (assumed) destructor for func on L12 keeps 's and thus 'y alive on L11 as well.

My example that used &dyn Fn(_) also made the lifetime part of the type, so that it also keeps 's directly alive.


My terminology is probably a bit loose, as things get very nuanced around concepts like "the opaque type captures the lifetime but does not contain the lifetime" -- as discussed in the appendix of the analysis linked above. This example now seems to me to be sort of "half way between" some of the examples and possible ways forward mentioned in that analysis -- an illustration of the borrow checker making some use of the implications of the 'static bound, just not all that's arguably possible. (This corresponds to an example from the analysis.)


  1. to minimize breakage, we needed a way to not capture every lifetime outside of traits ↩︎

  2. including appendix D ↩︎

  3. because it shows up in the type of a value (func) that is alive -- going to be used later ↩︎

2 Likes

This fails on 1.74 and works on 1.75 forward, which corresponds with this PR:

In the example above, we should be able to deduce from the 'static bound on capture's opaque that even though 'o is a captured region, it can never show up in the opaque's hidden type, and can soundly be ignored for liveness purposes.

So now I'm confident my take in the previous comment is correct; as of that PR, that borrow checker makes use of RFC 1214-esque conclusions for opaque type bounds in at least some cases (including + 'static).

You can dive the links in the PR for more interesting discussions :smiley:.

I think I get the point: + 'a + 'b is actually Hidden Type: 'a and Hidden Type: 'b which means return value is valid for union of the 'a and 'b. use<'a, 'b> means hidden type includes 'a and 'b so return value is valid for the intersection of the lifetimes. And rust will never add + 'a + 'b implicitly. Instead it will capture all generic para(lifetimes and types) in 2024 Edition.

But for your example, if there was an + 'a bound, what fails to compile should be definiton of fn example. I change fn example like that to satisfy + 'a bound, then two calls will both succeed.

Do you mean + 'a with implicit + use<T> led to T: 'a?

That's true, the definition of fn example that returns both a and b is what fails when there's a +'a or + 'b or both.

Approximately: you have to actually contain the lifetime in the underlying return type. So if you don't actually contain T, it's still okay, like your modification above that had a + 'a but still worked because you didn't return something containing b.

Or like how Fn(&'a) + 'static can work in some cases, even though 'a is captured.

1 Like

I hava read your reply again, which explains the problem perfectly. Now I can answer a more generalized question: When use<..> and + '_ exist at the same time, how the latter influence the former for return value? + 'long will "overlap" the restriction of use<'short> for return value. Similarly, + 'a + 'b makes compiler infer the return value is valid for union of 'a and 'b even there is use<'a, 'b>.

impl Trait<'short> + 'long might be more complex like our example impl Fn(&'a str) + 'static because the method we call involves lifetimes.

Also, I find a fun fact:

fn say_some<'a, 'b>(name: String) -> impl Fn(&'a str) + 'b {
    move |text| println!("{name} syas: {text}")
}

Adding 'b will have the same effect with 'static.

If I change your example like below, it would compile:

fn say_some<'a>(_name: String) -> impl Fn(&'a str) + Copy {
    |_| {}
}

fn main() {
    // Let's say the lifetime in the function call is `'s`
    let func = say_some("Blackbird".into());    // L1
    {                                           // L2
        let x = String::from("hello");          // L3
        // Let's call this lifetime `'x`        // L4
        func(&x);                               // L5

        let y = String::from("world");          // L6
        // Let's call this lifetime `'y`        // L7
        func(&y);                               // L8
    }                                           // L9
}                                               // L10

It's weird that the compiler didn't report an error... I guess 's doesn't include L1 which is initialization statement of func. But why?

PS: Using func("sth") between L9 & L10 or removing + Copy will cause the familiar error as expected: x does not live long enough.

It doesn't matter if 's contains L1 or not because borrows propogate forward in control flow. Even though 'y: 's, 'y doesn't need start until just before func is called on L8 for example. So you can do this and it will still compile:

        let mut y = String::from("world");      // L6a
        y.push('!');                            // L6b

In the modified example, x and y remain borrowed until the last use of func, which is before they go out of scope on L9. There's no use of func on L10 because the Copy bound ensures a trivial/non-existent destructor which cannot observe it's generics (i.e. it doesn't cause the lifetime to be alive), from a borrow checking perspective.

Calling func after L9[1] or removing the Copy bound both introduce a use of func after L9, causing x and y to be borrowed when they go out of scope, and thus an error.


  1. or even just moving it ↩︎

1 Like

Thank you for the reply:

I can understand why borrows propogate forward in control flow. That should be memory safe. But rustonomicon says:

'long <: 'short if and only if 'long defines a region of code that completely contains 'short

Does that mean the section is not rigorous?

And this similar example won't report an error:

fn main() {
    let name = new(); // adding ::<'static> also compiles
    {
        let word1 = String::from("talk is cheap");
        name.call(&word1);
    }
    {
        let word2 = String::from("show me the code");
        name.call(&word2);
    }
}

struct Struct<'a> {
    field: &'a str,
}

fn new<'a>() -> Struct<'a> {
    Struct { field: "blackbird" }
}

impl<'a> Struct<'a> {
    fn call(&self, other: &'a str) {
        println!("{} says {}", self.field, other);
    }
}

To some extent it's a matter of semantics, but generally speaking, a summary that short is just not able to spell out all the nuances. Frankly, I'm not sure if that statement holds or not for this specific example under NLL's definitions....

  • If we say 's must include any point where a 'z: 's constrain shows up, than by the NLL definitions, it's not true as 'x and 'y are disjoint but both have a "super type" relationship with 's
  • If we say 's may not include those points, than 's is the empty set and 'y: 's and 'x: 's trivially include 's entire region (nothing)

...but that's also moot because the propagation rule definitely allows a "super type" lifetime that doesn't contain the entire "subtype" region when it hits a discontinuity. However, it's still a matter of semantics, as NLL also already distinguishes between borrows and their associated lifetimes so that the borrow can end before the lifetime. Conceivably one could do something similar here.

Additionally, polonius will change subtyping to be location sensitive, making the exact definition of a lifetime even more subtle.

&Struct<'a> is covariant in 'a, so the lifetime can shrink to be as short as the method call duration at every call site. &mut Struct<'a> is invariant in 'a: change it to that and you'll get the borrow checker error you expected.

2 Likes

Thanks!!! Now there's only one last question:

Even bounds consider variance, I'm not sure what does this have to do with contravariance...

Do you mean impl Trait<'a> is invariant in 'a? The reference says dyn Trait<T> is invariant in T. I think the principle behind dyn and impl is the same. For instance, replacing Struct<'a> with impl Trait<'a> can also get the borrow checker error as expected:

fn main() {
    let name = new();
    {
        let word1 = String::from("talk is cheap");
        name.call(&word1);
    }
    {
        let word2 = String::from("show me the code");
        name.call(&word2);
    }
}

struct Struct<'a> {
    field: &'a str,
}

fn new<'a>() -> impl Trait<'a> {
    Struct { field: "blackbird" }
}

trait Trait<'a> {
    fn call(&self, other: &'a str);
}

impl<'a> Trait<'a> for Struct<'a> {
    fn call(&self, other: &'a str) {
        println!("{} says {}", self.field, other);
    }
}

Well, I found this instruction in Rust Compiler Dev Guide:

We only infer variance for type parameters found on data types like structs and enums. In these cases, there is a fairly straightforward explanation for what variance means. The variance of the type or lifetime parameters defines whether T<A> is a subtype of T<B> (resp. T<'a> and T<'b>) based on the relationship of A and B (resp. 'a and 'b).

We do not infer variance for type parameters found on traits, functions, or impls. Variance on trait parameters can indeed make sense (and we used to compute it) but it is actually rather subtle in meaning and not that useful in practice, so we removed it. See the addendum for some details. Variances on function/impl parameters, on the other hand, doesn't make sense because these parameters are instantiated and then forgotten, they don't persist in types or compiled byproducts.

Yeah, that is what I meant in this context. I phrased it that way because the Trait<'a> in -> impl Trait<'a> is a bound on the opaque return type. But it also applies to things like this:

// This compiles
pub fn contravariant<'a>(f: fn(&'a str)) {
    let _g: &dyn Fn(&'static str) = &f;
}

// This does not: `F` can not be assumed to implement `Fn(&'static str)`
pub fn invariant<'a, F: Fn(&'a str)>(f: F) {
    let _g: &dyn Fn(&'static str) = &f;
}

I think this is the abandoned "trait variance and vtable resolution" portion of the addendum linked in your dev guide quote. But it's hard to say...

  • That addendum pre-dates the use of dyn so it's hard to tell when they're talking about traits versus types
  • Their "go to example" for subtyping doesn't exist in Rust
  • The section on bounds has "vtable" in the title, but vtables shouldn't matter as the use of generics in this way results in monomorphization, not virtualization

...on the other hand it probably doesn't matter; if we get some sort of variance for bounds or opaque types or traits, it will almost surely be a fresh start, and not a revival of PhantomFn and all that.

1 Like