I did take a look at covariance and invariance in Rust, and I think I now have an idea what's happening. I tried again to understand things here:
As &'a mut &'b T
is invariant over 'b
(if I understand it right), we must ensure that the returned future (which captures it
)
- lives at most as long as
'a
- and lives exactly as long as
'b'
.
Thus, the returned future has lifetime 'b
, and 'a
must live at least as long as 'b
does. This leads to the following code:
#![feature(type_alias_impl_trait)]
use std::future::Future;
type FooRet<'c> = impl 'c + Future<Output = ()>;
fn foo<'a, 'b>(it: &'a mut &'b ()) -> FooRet<'b>
where
'a: 'b,
{
async move {
drop(it);
}
}
#[tokio::main]
async fn main() {
let v: () = ();
let mut r: &() = &v;
foo(&mut r).await;
}
This code compiles with my rustc (and also executes without a panic). Note that it does not seem necessary to provide more than one lifetime argument to FooRet
(which I named 'c
here) if I correctly provide the relationship between 'a
and 'b
as bounds to foo
.
An alternative seems to provide two lifetime parameters to FooRet
and elide the lifetime in impl
, as @Yandros pointed out:
In which case the above example would be written as:
type FooRet<'a, 'b> = impl Future<Output = ()>;
fn foo<'a, 'b>(it: &'a mut &'b ()) -> FooRet<'a, 'b> {
async move {
drop(it);
}
}
I hope I did get things right so far. Now to something @steffahn said:
Is there any authoritative reference regarding the behavior of impl
without explicit lifetimes? The chapter Lifetime elision in the Rust reference doesn't seem to say anything about it. When you say the rules are "weird and inconsistent/buggy", does that mean I should expect them to change in future? That would be an argument against using type FooRet<'a, 'b> = impl Future<…>
.
Well, @Yandros took part in that discussion; I didn't. And I'm afraid I'm super confused now . Any way to explain it in more simpler words? (I'll try to read that thread though, and try to follow up here.)
I really would like to understand the exact behavior with impl
regarding lifetimes, and using impl
in associated types, as I think this should allow us to have efficient async trait methods (with a clunky syntax yet).
Let me put it all together and let's modify (and simplify) my original example with all that gathered knowledge:
#![feature(generic_associated_types)]
#![feature(type_alias_impl_trait)]
use std::future::Future;
trait NumberChanger {
type ChangeRet<'a, 'b>: Future<Output = ()>;
fn change<'a, 'b>(&'a self, number: &'b mut i32) -> Self::ChangeRet<'a, 'b>;
}
struct AddOffset {
offset: i32,
}
async fn some_async_stuff() {
println!("Let's pretend we do some async stuff here.");
}
impl NumberChanger for AddOffset {
type ChangeRet<'a, 'b> = impl Future<Output = ()>;
fn change<'a, 'b>(&'a self, number: &'b mut i32) -> Self::ChangeRet<'a, 'b> {
async move {
some_async_stuff().await;
*number += self.offset;
}
}
}
#[tokio::main]
async fn main() {
let mut i: i32 = 17;
let changer = AddOffset { offset: 2 };
let future = changer.change(&mut i);
future.await;
assert_eq!(i, 19);
}
This code compiles and executes without any error using my rustc. Thus, we can avoid unnecessary heap allocations in async trait methods, can't we?
It only seems to be a matter of using some (yet?) unstable features and dealing with a clunky syntax. The syntax issue could be solved by a crate providing a macro, such as real-async-trait
, I guess, though real-async-trait
has too many limitations as of now. In real-life code, I'd rather deal with the syntax above, instead of not being able to use more complex lifetimes.
What do you think?