Lifetime semantics of -> impl Trait

-> impl Trait automagically captures generic type parameters in scope

fn foo<'lt, T>(lt: &'lt (), ty: T)
  -> impl Sized

is sugar (and the only stable-Rust syntax at the moment) for:

#![feature(type_alias_impl_trait)]

fn foo<'lt, T>(lt: &'lt (), ty: T)
  -> __ImplTrait<T>
// where the compiler has secretly generated:
pub type __ImplTrait<T> = impl Sized; // no `'lt` generic param!
  • with __ImplTrait<T> being a compiler-generated struct, of sorts, which is generic over <T> and thus fully "infected by T".

That is, it has the semantics of:

//! Pseudo-code

fn foo<'lt, T>(lt: &'lt (), ty: T)
  -> impl Sized + InfectedBy<T> // no `+ InfectedBy<'lt>` !

What the helium is InfectedBy<T>?

It's pseudo-code of mine to express the property that this struct __ImplTrait<T> would have: for any region 'r, if the return type is usable within that region, then so must T itself be:

for<'r> (
    impl Sized + InfectedBy<T> : 'r
    ⇒
    T : 'r
)

Although the practical/intuitive aspect of it is rather the contra-position:

for<'r> (
    T : !'r // let's assume this is syntax to say that `T : 'r` does not hold.
    ⇒
    impl Sized + InfectedBy<T> : !'r
)

To illustrate, let's pick T = &'t str, for instance:

  • beyond 't, instances of type T = &'t str are not allowed to be used.
  • Then it means that beyond 't, the returned -> impl Sized won't be allowed to be used either (no matter the function body!).

These -> impl Sized + InfectedBy<T> semantics are what allow the returned instance to contain ty: T inside it.

But notice how, despite the 'lt generic "lifetime" parameter in scope, there is no + InfectedBy<'lt> in the unsugaring I've shown!

  • Example

    The following works fine:

    fn nothing_doër<'lt>(_: &'lt String)
      -> impl Fn()
    {
        || ()
    }
    
    let f = {
        let local = String::from("…");
        nothing_doër(&local)
    };
    f() // ✅ OK
    

    But the following does not:

    fn nothing_doër<T>(_: T)
      -> impl Fn()
    {
        || ()
    }
    
    let f = {
        let local = String::from("…");
        nothing_doër(&local)
    };
    f() // ❌ Error, `local` does not live long enough!
    

    😬


-> impl Trait does not implicitly capture generic "lifetime" parameters in scope

This is not an oversight of mine (nor of the language), but rather a deliberate choice of the design of -> impl Trait. See the associated RFC for more info about it, and the rationale behind that choice.

The fact there is no + InfectedBy<'lt> means the returned instance won't be allowed to contain lt: &'lt … inside it.

fn foo<'lt, T>(lt: &'lt (), ty: T)
  -> impl Sized
{
    lt // ❌ Error, return type captures lifetime `'lt`
       //    which does not appear in bounds.
}
Error message
error[E0700]: hidden type for `impl Sized` captures lifetime that does not appear in bounds
  --> src/lib.rs:11:5
   |
8  | fn foo<'lt, T>(lt: &'lt (), ty: T)
   |        --- hidden type `&'lt ()` captures the lifetime `'lt` as defined here
...
11 |     lt
   |     ^^
   |
help: to declare that `impl Sized` captures `'lt`, you can add an explicit `'lt` lifetime bound
   |
9  |   -> impl Sized + 'lt
   |                 +++++
  • By the way that suggestion is not right, since it is not fully generalizable. See below for an example.

The workaround, currently, is to make the lifetime appear in bounds. If your trait in question does not give you room for that, then you can still achieve that by virtue of defining a helper trait for this, which happens to map quite nicely to my previous InfectedBy<…> pseudo-code.

  1. Define a helper generic-over-a-lifetime trait:

    /// Dummy empty marker trait.
    pub trait InfectedBy<'__> {}
    impl<T : ?Sized> InfectedBy<'_> for T {}
    
  2. Add it (+ InfectedBy<…>) to the bounds of the returned -> impl Trait:

    //! Real rust code
    
    fn foo<'lt, T>(lt: &'lt (), ty: T)
      -> impl Sized + InfectedBy<'lt>
    {
        lt // ✅ OK
    }
    

FWIW, you can imagine that this procedure is easily automatable through a macro:

About + 'lt, a seemingly shorter, but not always applicable, workaround

In general, people will reach for a simple + 'lt kind of bound (rather than the more cumbersome + InfectedBy<'lt>).

But if you have understood the meaning of + 'usability correctly, you should realize how this only works for simple cases:

/// ✅
fn simple_case<'lt>(lt: &'lt ())
  -> impl 'lt + Sized
{
    lt
}

/// ❌
fn does_not_work<'a, 'b>(a: &'a (), b: &'b ())
  -> impl 'a + 'b + Sized
{
    (a, b)
}
Error message
error: lifetime may not live long enough
  --> src/lib.rs:21:5
   |
18 | fn does_not_work<'a, 'b>(a: &'a (), b: &'b ())
   |                  --  -- lifetime `'b` defined here
   |                  |
   |                  lifetime `'a` defined here
...
21 |     (a, b)
   |     ^^^^^^ function was supposed to return data with lifetime `'b` but it is returning data with lifetime `'a`
   |
   = help: consider adding the following bound: `'a: 'b`

error: lifetime may not live long enough
  --> src/lib.rs:21:5
   |
18 | fn does_not_work<'a, 'b>(a: &'a (), b: &'b ())
   |                  --  -- lifetime `'b` defined here
   |                  |
   |                  lifetime `'a` defined here
...
21 |     (a, b)
   |     ^^^^^^ function was supposed to return data with lifetime `'a` but it is returning data with lifetime `'b`
   |
   = help: consider adding the following bound: `'b: 'a`

help: `'a` and `'b` must be the same: replace one with the other

Indeed, + 'usability means that it has to be usable (at least) within the whole 'usability region!

So if you write + 'a + 'b it means that it will be usable within 'a and1 that it will be usable within 'b as well. That is, (an instance of (type)) impl 'a + 'b has to be usable within the union of the regions 'a and 'b.

1

that's exactly what + means in Rust: + adds capabilities, rather than restraining them, so there is no way + 'a + 'b yields, to the caller, something more restrictive than just + 'a.

But the (a, b): (&'a (), &'b ()) value we are returning is meant to be unusable whenever:

  • we leave the 'a region (because of its a component),
  • we leave the 'b region (because of its b component).

Which means (a, b) is only usable within the intersection of the 'a and 'b regions.

  • In expiry dates parlance, imagine a small bottle of milk, a, with an expiry date of one month, and another such bottle, b, but this time with an expiry date of one week. Say now that you mix the contents of both into a big bottle (this would be our (a, b) Rust item). Then the resulting mix should be drunk or thrown away within the week, lest the b part inside it go sour. That is, the resulting mix has an expiry date of 'b = min('a, 'b) = intersection('a, 'b).

    An impl 'a + 'b bottle of milk would, on the other hand, have meant that it would have been drinkable both before next week, and before next month, so effectively before next month ('a = max('a, 'b) = union('a, 'b)).

What we want to say is precisely that (a, b) is infected by both 'a and 'b, i.e., that it is InfectedBy<'a> + InfectedBy<'b>:

fn example<'a, 'b>(
    a: &'a str,
    b: &'b str,
) -> impl Fn(bool) + InfectedBy<'a> + InfectedBy<'b>
{
    move /* (a, b) */ |choice| {
        println!("{}", if choice { a } else { b })
    }
}

Granted, the previous function can be simplified:

fn example<'intersection_of_a_and_b>(
    a: &'intersection_of_a_and_b str,
    b: &'intersection_of_a_and_b str,
) -> impl 'intersection_of_a_and_b + Fn(bool)
  • since 'a and 'b, from the call-sites, would be able to shrink down to some 'intersection_of_a_and_b region smaller than both, which would bring us back to a single-lifetime scenario, and thus to using the convenient + 'usability syntax to get the returned existential to be infected by 'intersection_of_a_and_b.

But in more complex APIs, lifetimes may not be able to shrink, and a proper solution needs to be used if you are to make it work.

For instance, no matter how hard you try, there is no simpler signature for the following function:

fn impl_<'a, 'b>(
    a: Arc<Mutex<&'a str>>,
    b: Arc<Mutex<&'b str>>,
) -> impl Fn(bool) + InfectedBy<'a> + InfectedBy<'b>
{
    move /* (a, b) */ |choice| {
        println!("{}", if choice {
            *a.lock().unwrap()
        } else {
            *b.lock().unwrap()
        });
    }
}

Comparison to dyn Trait

Note that dyn Trait is actual type erasure, so if you manage to get your hands on the 'intersection lifetime of all the captured lifetime and type parameters, then you can simply use + 'intersection and Rust won't complain even when bigger unshrinkable lifetimes are part of the before-erasure type (in other words, type erasure is kind of able to shrink even otherwise unshrinkable lifetimes):

fn dyn_<'i, 'a : 'i, 'b : 'i>( // intersection('a, 'b) ⊇ 'i
    a: Arc<Mutex<&'a str>>,
    b: Arc<Mutex<&'b str>>,
) -> Box<dyn 'i + Fn(bool)>
{
    Box::new(move /* (a, b) */ |choice| {
        println!("{}", if choice {
            *a.lock().unwrap()
        } else {
            *b.lock().unwrap()
        });
    })
}

The keen eye may have spotted the "intersection lifetime" pattern. Otherwise, that newly introduced 'i "lifetime" parameter will probably seem weird or confusing, in which case you may want to check out the dedicated section.