Search code examples
rustasync-awaitlifetimegeneric-associated-types

Why does this combination of GATs, lifetimes, and async require `T: 'static`?


This odd bit of code emits an error from do_the_thing() saying T does not live long enough:

use std::future::Future;

trait Connection: Send {
    type ExecFut<'a>: Future<Output = ()> + Send
    where
        Self: 'a;

    fn transaction<F>(&mut self, _f: F)
    where
        F: for<'a> FnOnce(&'a mut Self) -> Box<dyn Future<Output = ()> + Send + 'a> + Send,
    {
        unimplemented!()
    }

    fn execute<'a>(&'a mut self) -> Self::ExecFut<'a> {
        unimplemented!()
    }
}

fn do_the_thing<T: Connection>(connection: &mut T) {
    connection.transaction(|conn| {
        Box::new(async move {
            conn.execute().await;
        })
    });
}
error[E0310]: the parameter type `T` may not live long enough
  --> src/main.rs:22:9
   |
22 | /         Box::new(async move {
23 | |             conn.execute().await;
24 | |         })
   | |__________^ ...so that the type `T` will meet its required lifetime bounds
   |
help: consider adding an explicit lifetime bound...
   |
20 | fn do_the_thing<T: Connection + 'static>(connection: &mut T) {
   |                               +++++++++

See it on the playground.

As far as I can tell, there should be no implied bound that 'static is required. Pretty much everything is constrained by 'a and any lifetimes in T should always outlive it regardless. Though obviously there might be something I'm missing.

I've also found a few "solutions" that aren't helpful:

  1. Adding 'static as the compiler suggests is not reasonable. It can be worked around but there are real Connections with non-'static lifetimes that I would like to use.
  2. For some reason removing the Send bound on the dyn Future makes it pass compilation. This makes zero sense to me.
  3. If I make the function non-generic but use a Connection that is not 'static it compiles as well, seemingly contradicting the compiler error.

The above is not real code; the original motivation stems from using AsyncConnection from the diesel-async crate and similar code structure. However, I hope it is representative of the core issue and that by understanding the problem and potential solutions here, they can be adapted.


Solution

  • From further tinkering and research, I found this issue - GATs: Decide whether to have defaults for where Self: 'a - in regard to the where clause on type ExecFut<'a>. From the comments in the issue, it seems others have had similar errors from having this bound enforced.

    There's a workaround provided from the issue, which is to split your trait into two - one for the associated types and another for the methods so that the where Self: 'a clause can be omitted:

    This breaks my code. Workaround?

    First, if any code breaks from adding the required bounds, we really want feedback. Second, the workaround is to move the GAT into a super trait. Using the example above, our new code would look like:

    trait IterableSuper {
        type Item<'x>;
    }
    trait Iterable: IterableSuper {
        fn iter<'a>(&'a self) -> Self::Item<'a>;
    }
    

    Doing that on my example makes it compile but it does start to get messy in my target use-case.

    Another workaround that I've seen mentioned was to introduce a dummy function that uses the associated type but not in conjunction with self. This allows you to omit the where Self: 'a clause as well, and thus the code compiles:

    trait Connection: Send {
        type ExecFut<'a>: Future<Output = ()> + Send;
     // ^^^^^^^^^^^^^^^^ does not need `where Self: 'a`
    
        fn transaction<F>(&mut self, _f: F)
        where
            F: for<'a> FnOnce(&'a mut Self) -> Box<dyn Future<Output = ()> + Send + 'a> + Send,
        {
            unimplemented!()
        }
    
        fn execute<'a>(&'a mut self) -> Self::ExecFut<'a> {
            unimplemented!()
        }
        
        fn _disable_lint_for_exec_fut<'a>(_: Self::ExecFut<'a>) { }
     // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ dummy function
    }
    

    As a side note, there is another issue linked - Problems with GATs, async, and Send-bounds - that describes an odd interaction between the bounds like above and Send which may explain why removing a Send bound caused the snippet to compile. Still weird.