A lifetime problem vs. associated type (or generic parameter)


I have a problem with lifetime bounds that I was not able to resolve.

The simplified code:

trait Bar { 
    type Assoc;

impl<'a, T, A> Bar for (T, fn(T) -> Box<dyn Bar<Assoc = A> + 'a>) {
    type Assoc = A;

fn wrap<'a, T: 'a, A>(value: T, func: fn(T) -> Box<dyn Bar<Assoc = A> + 'a>) -> Box<dyn Bar<Assoc = A> + 'a> {
    Box::new((value, func))

The error:

error[E0309]: the parameter type `A` may not live long enough
  --> src/main.rs:22:5
21 | fn wrap<'a, T: 'a, A>(value: T, func: fn(T) -> Box<dyn Bar<Assoc = A> + 'a>) -> Box<dyn Bar<Assoc = A> + 'a> {
   |         -- the parameter type `A` must be valid for the lifetime `'a` as defined here...
22 |     Box::new((value, func))
   |     ^^^^^^^^^^^^^^^^^^^^^^^ ...so that the type `A` will meet its required lifetime bounds
help: consider adding an explicit lifetime bound
21 | fn wrap<'a, T: 'a, A: 'a>(value: T, func: fn(T) -> Box<dyn Bar<Assoc = A> + 'a>) -> Box<dyn Bar<Assoc = A> + 'a> {
   |                     ++++

For more information about this error, try `rustc --explain E0309`.

I do not understand where does the lifetime requirement come from. In my actual code there are more associated types (like Bar::Assoc), some are used as input to associated functions, some as output, some are used only for their associated items and some are there only to carry the type information. In any way there should be (i.e. that is my intention) no relation between A and 'a, at least not in this fragment of code.

So ... how can I make it work without the A: 'a bound? Or is there something I am missing?

... I mean ... everything works (so far) when I just add the bound as the compiler suggests, but it seems completely arbitrary and does not make sense to me ...

I'm not sure I understand it correctly, but let me try:

first, if you examine the type signature a bit, you'll find there are "logically" different levels of references (hence different lifetimes) involved:

fn wrap<'a, 'b, T, A>(
    value: T,
    func: fn(T) -> Box<dyn Bar<Assoc = A> + 'a>,
) -> Box<dyn Bar<Assoc = A> + 'b>;

then, because you want to return the tuple (value, func), they must have lifetime bound 'b as required by the return type, which means both value and func must outlive 'b, translate to types:

  • for value, the type T: 'b
  • for func, the function pointer type fn(T) -> Box<dyn ... + 'a>: 'b, which in turn requires:
    • dyn Bar<Assoc = A>: 'b
    • 'a: 'b

observe that because Box is covariant, you can replace the 'a with 'b, so the type of func can be fn(T) -> Box<dyn Bar<...> + 'b, and you'll see 'a is not used anymore so just remove it, you get back the original type signature with a single lifetime.

the compiler suggest A: 'a is actually derived from dyn Bar<Assoc = A>: 'a. I believe associated types are invariant with regard to Self, but I'm not an expert and could well be wrong, don't quote me on this.

you can make it more obvious if you write the bound explicitly

fn wrap<'a, T, A>(
    value: T,
    func: fn(T) -> Box<dyn Bar<Assoc = A> + 'a>,
) -> Box<dyn Bar<Assoc = A> + 'a>
    T: 'a,
    dyn Bar<Assoc = A>: 'a,
    Box::new((value, func))

or, equivalently, you can give the trait object an explict name (alias) too:

type DynBar<'a, A> = dyn Bar<Assoc = A> + 'a;

fn wrap<'a, T, A>(value: T, func: fn(T) -> Box<DynBar<'a, A>>) -> Box<DynBar<'a, A>>
    T: 'a,
    DynBar<'a, A>: 'a,
    Box::new((value, func))

Thanks for the analysis, I mostly agree with it, but:

  • where does the invariance come from?
  • what does it have to do with variance anyway? As far as I understand, variance affects coercion, not bounds propagation. And the only coercion happening here is the one from <(T, fn(...)) as Bar<Assoc = A>> to dyn Bar<Assoc = A> + 'a which happens on the type constructor, not its argument.
  • I have more uses of dyn Bar<Assoc = A> + 'a in my code, but this error happened only after I introduced the fn(T) -> Box<...> into the mix.
  • After I added the lifetime bounds they did not bubble up through the rest of my generic code, so maybe there is some relation between an associated type and its implementor?

Now I feel like my Rust understanding is full of 'maybe's, 'I think's, 'it kind of makes sense's etc. I would like to see some more grounded description/explanation.

This is true; bounds don't care about variance, and variance doesn't matter in the OP.

I'll give it as shot.

In order to type erase a base type X into a dyn Trait + 'x, the bound X: 'x must be met. In wrap, you're trying to type erase a (T, fn(T) -> Box<dyn Bar<Assoc = A> + 'a>) into a dyn ... + 'a.

The outlives bound can be broken down syntactically. Here, the required bound and it's implications are:

(T, fn(T) -> Box<dyn Bar<Assoc = A> + 'a>): 'a
    T: 'a
    fn(T) -> Box<dyn Bar<Assoc = A> + 'a>: 'a
        T: 'a
        Box<dyn Bar<Assoc = A> + 'a>: 'a
            'a: 'a
            dyn Bar<Assoc = A>: 'a
                A: 'a

wrap didn't require A: 'a,[1] so the type erasure was not allowed.

And that's really it as far as the error goes. The main potential confusions I can anticipate are

I don't have a good answer for the last one. If the trait itself has generic parameters, it can be the case that the associated type doesn't outlive 'a; however when there are none (as in this example), it shouldn't be possible.

If you want to type erase something with A in the type to dyn ...+ 'a, the A: 'a relationship must hold. That would be true even if A: 'a was implied (so you didn't have to state it explicitly).

  1. and that bound isn't implied by anything else ↩ī¸Ž

1 Like

Thank you for thorough explanation and references.

I still feel not 100% sold on syntactical approach to outlives bounds, but the more I think of it, it kind of starts to make sense.