What a contract like fn('a) -> 'a + 'static means?

I found the post, and notice it mentioned RPIT may introduce undesirably tight bounds

RPIT captures the lifetimes of any generic type parameters that are in scope. This can cause the returned hidden type to have undesirably tight bounds. For example, this code does not work, even though the returned hidden type clearly does not capture any references:

fn foo(_t: T) -> impl Sized { () // Returned hidden type captures lifetimes in T. } fn bar<'x>(x: &'x ()) -> impl Sized + 'static { foo(x) // Type captures 'x. }

error: lifetime may not live long enough
  |
  | fn bar<'x>(x: &'x ()) -> impl Sized + 'static {
  |        -- lifetime `'x` defined here
  |     foo(x) // Type captures 'x.
  |     ^^^^^^ returning this value requires that `'x` must outlive `'static`

There are no good ways to work around this in stable Rust today. As we'll see below, TAIT gives us a way to express the correct bounds for this hidden type.

Then I tried the following, it compiles, but seems to me it breaks the life bound rule:

fn foo<T>(_t: T) -> impl Sized + 'static {
    ()
}

fn bar<'x>(x: &'x i32) -> impl Sized + 'x + 'static  {
    foo(x)
}

fn main() {
    let k = {
        let z = 9;
        bar(&z)
    };
    println!("{}", std::mem::size_of_val(&k));
}

To my understanding, a fn('a) -> 'a means a lifetime contract: the returned value holds a same life time as the passed argument, so, as long as the returned value lives, the argument lives.

Well, I don't know what a the contract of a fn('a) -> 'a + 'static mean.
I prefer to explains the returned value has a contract of holding a shorter lifetime because min('a, 'static) satisfies both 'a and 'static.

But the result shows it is not, it is max('a, 'static).
I don't know whether the behavior is desirable.

I think you are being tripped up by variance? Function return types are covariant, while arguments are contravariant. If you put the impl Sized + 'x + 'static annotation in argument position, it doesn't compile.

It is not about variance
The contravariance only takes effect when considering a function as a whole, e.g. fn1 = fn2 or something like this.

That argument doesn't really work; how could you then explaing that calling fn(&'short T) with an argument &'long T works, but assigning a function returning &'short T to a binding typed &'long T does not? It's all about the direction of data flow, hence variance.

That's also the source of the difference between APIT and RPIT:

  • APIT creates an input type (using an anonymous type parameter), and if that type requires &'short T, you can still pass a &'long T but not vice versa;
  • RPIT creates an output type (using an existential) that promises to uphold all specified trait and lifetime bounds, so if it promises 'static + 'short, then it has to uphold both 'static and 'short. This is, again, exactly the kind of data flow-related directionality that underlies variance.

To see why this is true, consider how variance is defined: it's defined in terms of subtyping relationships. In the example above, impl Sized + 'static is a subtype of impl Sized + 'x. That's why an argument of type impl Sized + 'x can accept a value that satisfies only 'x but impl Sized + 'static can't; and conversely, a return value typed impl Sized + 'static can be assigned to a place typed impl Sized + 'x but not vice versa.

This directionality is the defining characteristic of the variance of functions, even if there's no subtying involving the type of the function as a whole per se.

Can't understand.
Let's talk about the direction: both direction is from argument --> parameter.

At argument position, 'long (argument) ---> 'short (parameter) is convariant.

At returned position, the argument is the real returned value, the parameter is the returned value in the signature. then 'long (argument) ---> 'short (parameter) still holds, it is convariant.

So, both is convariant; no contravariant at either site; Your introducing contravariant here means nothing, you code can't be compiled because of convariant, not contravariant.

All above is not my concern in this post though. My question is about:

I totally agree that, but the result shows only 'static uphold in my code.

So, the returned value .e.g. k continues to exist after z getting dropped.

I'm not sure what you mean – you are making an unclear/unusual distinction between "argument" and "parameter", furthermore, I was clearly referring to the return type in the second case, in which case no argument or parameter is involved whatsoever.

That's a nonsensical statement, because 'static implies 'x, ie. the 'x is automatically upheld if 'static is. You simply can't have a 'static value that is not also 'x for any choice of 'x, it is a contradiction.

'static is the longest lifetime of all, and you can't create a type that is valid for a long time but not valid for a shorter type. Just like you can't concoct a number that is larger than 10 but smaller than 5, for example.

1 Like

You seem to be expecting that 'x means "exactly that lifetime, nothing more, nothing less". That's only the case when a type is invariant in the appropriate lifetime parameter. If it's covariant, then 'x also allows 'longer_than_x. If it's contravariant, it also allows 'shorter_than_x.

Or maybe you are expecting an RPIT to be generic? That's not the case, either — an RPIT denotes a single, concrete type. You can't have an RPIT that is declared as 'static but then sometimes behaves as if it weren't, depending on the context.

1 Like

The question is not about variance.
It is about reborrow: How the reborrowed argument and the returned value make a deal at the lifetime contract.

There's no reborrowing happening in the code whatsoever. You are likely using that expression incorrectly; I unfortunately am unable to decipher what exactly you mean by it.

But let's phrase this differently: if you declared the return type as 'static and hence valid FOREVER, by definition, then why do you think you should be unable to assign the return value to a place that outlives an unrelated, shorter lifetime?

They don't, as they are unrelated (because of the 'static in the return type).

If the code compiles, they don't.

fn bar<'x>(x: &'x i32) -> impl Sized + 'static  {
    foo(x)
}

If the code compiles only with an addtional 'x

fn bar<'x>(x: &'x i32) -> impl Sized  + 'x + 'static  {
    foo(x)
}

They are related.
That is also the post I mentioned in github talks about.

To my understanding, rust seems preferring they are related (by forcing add 'x in the signature to make it compiling), and seems the code below should fail to compile, because the returned value of bar(&z) should be limited at the inner scope.

fn main() {
    let k = {
        let z = 9;
        bar(&z)
    };
    println!("{}", std::mem::size_of_val(&k));
}

But the result shows it turns out they are unrelated (as you say). The effort of forcefully adding 'x is easily erased by adding another 'static.

To me, that is a jail breaking.
That is my confusion and why I asked here .

Wrong; that is merely an artefact of the implementation of the function (and ultimately, the declaration of foo). It's an error that concerns the implementor of the function, not the caller thereof.

IIRC it's the result of some limitation and/or an overzealous, robustness-related behavior of the compiler, which mechanically (and thus, overly cautiously) makes the implementor be explicit about the lifetime in the argument (not to cause accidental breaking changes by merely modifying the body and thereby modifying the capture structure of the return type), even though the trait/lifetime solver clearly understands that the shorter lifetime is redundant when looking at the function from the outside.

Accordingly, if you remove the implicit captured lifetime (and the implied but unintended dependence from the return value of foo() — and not bar() itself!) then it compiles just fine.

I'm not sure the function does what you think it does. From the question and discussions, you seem to think that bar is returning an opaque function pointer or function item type. But it's actually returning an opaque () -- the output type of foo::<&'x i32>. (): 'static and (): 'a for any lifetime 'a, so the bounds are met.

In contrast this does indeed cause a borrow check error.

 fn bar<'x>(x: &'x i32) -> impl Sized + 'x + 'static  {
-    foo()
+    foo::<&'x i32>
 }

I think this isn't actually the point of the example, but anyway...

A for<'any> fn(&'any i32) -> &'any i32 function pointer would satisfy a 'static bound, but a fn(&'a i32) -> &'a i32 function pointer would not (unless 'a: 'static). With the diff, you have something (a function item type) akin to the latter.


The variance discussion also looks like a distraction to me. fn(&'a i32) -> &'a i32 is invariant in 'a because 'a appears as both an argument (contravariant in isolation) and return (covariant in isolation).

Opaque types are invariant in any lifetimes or types they capture.


This is probably the crux of question. As covered in that post, opaque return types capture generic arguments that are in scope. In particular, outside of traits and async on edition 2021, they capture all generic types, but only capture generic lifetimes that are somehow tied to the signature.

But generic types can contain lifetimes, and those do get captured. So foo::<&'x i32>'s return type captures &'x i32. However from the signature alone,

fn bar<'x>(x: &'x i32) -> impl Sized + 'static

bar is not allowed to capture 'x.[1] You need to tie 'x to the return type somehow.

Adding a + 'x bound works and -- although the example you linked says "there's no good work around" -- adding that bound is actually a perfectly fine work around for the specific example. A type that satisfies a 'static also satisfies any 'x bound and there are no unwanted bounds being applied to other parameters.

Let's see if this becomes more clear with some extra annotation. Non-working version:

pub type FooReturn<T> = impl Sized + 'static;
pub fn foo<T>(_t: T) -> FooReturn<T> {
    ()
}

// Edition 2021: Don't capture unmentioned lifetime parameters
//
// Implication: Type can't vary based on lifetime (there can be
// only exactly one type in this case due to 0 parameters)
pub type BarReturn = impl Sized + 'static;
pub fn bar<'x>(x: &'x i32) -> BarReturn {
    foo(x)
}

// It doesn't work for reasons related to why you can't
// write this alias.
type BarLikeReturn = FooReturn<&'??? i32>;

Working version:

pub type FooReturn<T> = impl Sized + 'static;
pub fn foo<T>(_t: T) -> FooReturn<T> {
    ()
}

// Now parameterized by a lifetime (be it because you're on
// edition 2024, or because you added `+ 'x` to `bar`)
pub type BarReturn<'a> = impl Sized + 'static;
pub fn bar<'x>(x: &'x i32) -> BarReturn<'x> {
    foo(x)
}

// Now we can also write this alias.
type BarLikeReturn<'a> = FooReturn<&'a i32>;

Is that more clear?


This version works because the T captured by foo no longer includes the lifetime.

pub type FooReturn<T> = impl Sized + 'static;
type BarLikeReturn = FooReturn<i32>;

(Arguably foo should be rewritten to not capture anything at all, but as I understand it, even with precise capturing we won't have the ability to not capture generic types in the near future.)


  1. On edition 2021. Next edition bar captures 'x implicitly and so it works. ↩ī¸Ž

2 Likes

That is exactly what I can't understand;
Due to the definition, T<'a> means T can't outlives 'a, it meybe be something like

struct T<'a> {
    x: &'a String
}

a T<'a> can't exists after T<'a>.x getting out of it's scope, but here, T<'a> can continue to exist.

No, it's perfectly possible in the case of aliases, GATs, trait implementors, and the like. Those have more ways to prove lifetimes bounds than their syntactical path.

// This doesn't prevent `String: 'static` from being met
type Alias<'a> = String;

// And since they're the same thing, doesn't prevent
// `Alias<'a>: 'static` from being met
fn example<'s>(_: &'s str) where Alias<'s>: 'static {}

fn main() {
    let local = String::new();
    example(&*local);
}

It is true that opaques (-> impl Trait) add a wrinkle: they don't normalize to the underlying type and their "actual validity" doesn't leak. For those, you have to add the desired bound to the declaration.

#![feature(type_alias_impl_trait)]

pub mod scope_control {
    pub(crate) type Alias<'a> = impl Sized + 'static;
    fn define<'a>() -> Alias<'a> { String::new() }
}

use scope_control::Alias;

fn example<'s>(_: &'s str) where Alias<'s>: 'static {}

fn main() {
    let local = String::new();
    // works... but not if you remove `+ 'static` from `Alias<'a>`
    example(&*local);
}

Similar to how an associated type projection can meet lifetime bounds from "the environment" -- such as a bound on the associated type declaration -- an opaque lifetime bound can be met when it's part of the definition.

2 Likes

Or here's another example using GATs and generics. The body of example is checked before monomorphization, so the GAT cannot be normalized -- during checking, it remains a

<T as Trait>::Gat<'t>

But that doesn't mean that it can't meet a 'static bound, so long as there's something besides the path that can be used to prove that bound. And there is: the + 'static on the Gat<'a> declaration.

If you remove the + 'static, example throws an error.

(If you follow the compiler advice,[1] you introduce a 't: 'static bound, which in turn implies a T: 'static bound, at which point Gat<'a> can be proven "from the components" as per the previously linked RFC. But adds an unwanted constraint to calling example.)


  1. modulo syntax errors ↩ī¸Ž

1 Like

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.