Trouble understanding conflicting lifetime requirements with generic types


#1

Steps to reproduce / TL;DR:

Minimal problem repo:

git clone git@github.com:azriel91/amethyst_menu_experiment.git
cd amethyst_menu_experiment
git checkout experiment/create-next-state-from-fn
cargo build

Background:

I’m using the Amethyst game engine to build a game. It uses a stack system to go between States, and whether it pushes / pops / switches the active State depends on which Transitions you return from the current State.

Problem

I have a demo_setup::State that returns an arbitrary amethyst::States by running a function it is given in its constructor. I’m unable to create the demo_setup::State and feed it into a Transition; Rust tells me it can’t infer a lifetime given the constraints:

pub fn trans<'a, 'b>(&self) -> Trans<GameData<'a, 'b>> {
    match *self {
        Index::StartGame => Trans::Push(Box::new(other::State::new())),
        Index::Demo => {
            let next_state_fn = Box::new(|| -> Box<amethyst::State<GameData<'a, 'b>>> {
                Box::new(other::State::new())
            });
            let demo_setup_state = demo_setup::State::new(next_state_fn);
            Trans::Push(Box::new(demo_setup_state))
        }
        Index::Exit => Trans::Quit,
    }
}

Error from cargo build

   Compiling amethyst_menu_experiment v0.1.0 (file:///home/azriel/work/github/azriel91/amethyst_menu_experiment)
error[E0495]: cannot infer an appropriate lifetime due to conflicting requirements
  --> src/main_menu/index.rs:35:38
   |
35 |                 Trans::Push(Box::new(demo_setup_state))
   |                                      ^^^^^^^^^^^^^^^^
   |
note: first, the lifetime cannot outlive the lifetime 'a as defined on the method body at 27:5...
  --> src/main_menu/index.rs:27:5
   |
27 |     pub fn trans<'a, 'b>(&self) -> Trans<GameData<'a, 'b>> {
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   = note: ...so that the types are compatible:
           expected std::boxed::Box<amethyst::State<amethyst::GameData<'a, 'b>>>
              found std::boxed::Box<amethyst::State<amethyst::GameData<'_, '_>> + 'static>
   = note: but, the lifetime must be valid for the static lifetime...
   = note: ...so that the expression is assignable:
           expected std::boxed::Box<amethyst::State<amethyst::GameData<'a, 'b>> + 'static>
              found std::boxed::Box<amethyst::State<amethyst::GameData<'a, 'b>>>

error: aborting due to previous error

I kind of know what lifetimes are, but I don’t understand a few things:

  • What is imposing the 'static lifetime constraint. Is it because the Transition is returned by a function on the Index enum?
  • Can I remove that constraint?
  • What lifetimes in generic types mean — I had the demo_setup::State take in Fn -> amethyst::State<GameData<'a, 'b>> before, but that made the state need to have + 'static (why? Is it because the state that the function returns could reference a GameData from the function? I couldn’t figure it out.).

I tried sprinkling as Box<amethyst::State<GameData<'a, 'b>> + 'a> in various places but couldn’t make sense of the error messages before I became frustrated =/


#2

It looks like the 'a and 'b lifetime params are unbound in trans(). It’s a method that takes a mutable borrow of self for some (elided) lifetime, and claims it returns Trans<GameData<'a, 'b>> - but those lifetimes aren’t associated with Index itself nor the borrow of self. So all they can really be here are 'static. And in fact, 'static is correct because it looks like there’s nothing but owned values being returned.

So:

pub fn trans(&self) -> Trans<GameData<'static, 'static>>

is the correct (I believe) signature. Then, strip the lifetime params in here too and replace with 'static:

impl amethyst::State<GameData<'static, 'static>> for State {
    ...
    fn update(&mut self, data: StateData<GameData>) -> Trans<GameData<'static, 'static>> {
        data.data.update(&data.world);
        let menu_event_channel = &data
            .world
            .read_resource::<EventChannel<MenuEvent<main_menu::Index>>>();

        let mut reader_id = self
            .menu_event_reader
            .as_mut()
            .expect("Expected menu_event_reader to be set");
        let mut storage_iterator = menu_event_channel.read(&mut reader_id);
        match storage_iterator.next() {
            Some(event) => match *event {
                MenuEvent::Select(index) => index.trans(),
                MenuEvent::Close => Trans::Quit,
            },
            None => Trans::None,
        }
    }
}

As for 'static lifetime in your error message, that’s imposed by the default object bounds. In other words, given Box<SomeTrait> (i.e. a trait object), it’s really Box<SomeTrait + 'static>. But really, the issue is the use of unbound lifetime parameters 'a and 'b.


#3

That worked, thank you very much :bowing_man:!

I guess for functions with lifetime parameters, the lifetime parameters should make sense as part of the function definition, and shouldn’t rely on the caller for context for the parameters to make sense.

Is it correct that, impl amethyst::State<GameData<'static, 'static>> for State work with code that expects amethyst::State<GameData<'a, 'b>>?

  • By definition,'a and 'b (or any other lifetime) must be shorter or equal to 'static right?
  • As long as the Trans<GameData<'static, 'static>> is constructed with owned values, it doesn’t violate the lifetime requirement
  • If I do have a GameData<'a, 'b> used in a returned State, then that’s when the trans() method could have bounded lifetimes 'a and 'b,

I’m eternally grateful + impressed — that took you mere minutes; I spent so many hours and still hadn’t solved it.


#4

To be honest, there’s probably a way to make your code work with trans<'a, 'b> as well, but I’d need to play with it. The 'static sprung to mind because you’re not borrowing any data and that simplifies the code/signatures.

Yup. As long as subtyping/variance is allowed in the places you use GameData<'a, 'b>, a GameData<'static, 'static> is a subtype of any GameData<'a, 'b>.


#5

So I was curious to see if it’s possible to make it work with the trans<'a, 'b> flavor that you started with, and I think I’ve come to the conclusion that it bumps up against the same “traits with a generic type parameter, when put in a trait object, are invariant over the type parameter” restriction as mentioned here.

There’s this trait there:

pub trait State<T>

Your code then does:

Trans::Push(Box::new(demo_setup_state))

which is using this enum:

/// Types of state transitions.
pub enum Trans<T> {
    /// Continue as normal.
    None,
    /// Remove the active state and resume the next state on the stack or stop
    /// if there are none.
    Pop,
    /// Pause the active state and push a new state onto the stack.
    Push(Box<State<T>>),
    /// Remove the current state on the stack and insert a different one.
    Switch(Box<State<T>>),
    /// Stop and remove all states and shut down the engine.
    Quit,
}

And although GameData<'static, 'static> is a subtype of GameData<'a, 'b>, this variance is “lost” once that Box<State<T>> inside Push comes into play.

Doing things with GameData<'static, 'static>, as in my initial post, nicely sidesteps this problem because everything is dealing with the 'static lifetimes there, and no variance is needed.

At least this is how I explained it to myself :slight_smile:. Would be happy to have someone confirm/deny this.

If this is indeed true, then this is a fairly scary dark corner that one can design themselves into without realizing it until it’s a bit late in the game.