smol-rs / futures-lite

Futures, streams, and async I/O combinators.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Custom lifetimes for Boxed and BoxedLocal

jmmv opened this issue · comments

I just discovered the futures-lite crate and I thought "let me try to convert some trivial code that I have using futures to this lighterweight crate". And it's great because I trimmed 20 seconds of build time!

Unfortunately, the conversion wasn't as clean as I hoped because BoxedLocal currently seems to enforce a 'static lifetime... and I have a piece of code that was previously using LocalBoxFuture and requires a different lifetime. I was able to easily workaround this by redefining BoxedLocal to take a custom lifetime and then directly using Box::pin instead of FuturesExt::boxed_local(). See this commit.

But I think it'd be nice if the interface in futures-lite supported a custom lifetime on its own for both Boxed and BoxedLocal. Thoughts?

I personally never needed a boxed future that wasn't 'static. :)

A question: why is your function returning a LocalBoxFuture<'a, Result<()>>? Why not define an async fn so that the future is an anonymous type? I suppose because you want to be the return type to be nameable? If so, why not define a new future type, e.g. DoIfFuture<'a>?

The main drawback I see here is that the future is always boxed, even if it is used temporarily on the stack. But also, this might not be a problem when impl Trait syntax becomes usable in more places than today - and by that I mean type Foo = impl Trait syntax.

If you are suggesting to remove the boxed futures and simply declare all those functions as async fn... I wasn't able to do that because:

recursion in an `async fn` requires boxing

recursive `async fn`

note: a recursive `async fn` must be rewritten to return a boxed `dyn Future` rustc(E0733)

exec.rs(242, 65): recursive `async fn`

To be honest, this is the first async code I wrote in Rust, so I might be missing something obvious.

Ah, I see now. This seems unfortunate. :/

A way to solve this is to parametrize Boxed<T> over a lifetime like so: Boxed<'a, T>. To do this change, we'd have to bump the major version of futures-lite since it's a breaking change. My other reservation is that this might be a sort of niche case that makes Boxed<'static, T> a bit uglier than it is right now.

Another solution, which isn't a breaking change, would be to change the return type of boxed() and boxed_local() functions so that they return a lifetime-parametrized type. See here for a proof of concept: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=344083ee923760405baffb7e6ba961d5

I published futures-lite v1.1.0 that parametrizes boxed() and boxed_local() over a lifetime.

Now you can keep writing async fn. The trick is then to call such an function, box its returned future, and then call it. For example:

let f = self.do_if(branches).boxed_local();
f.await?;

So the idea is that the caller of the function boxes the future rather than the function itself.

Hey Stjepang, thanks for your attention so far.

I gave this a try and it seems to work. Thanks! But I'm confused about why the let f indirection is necessary. If I combine the two lines such that they say boxed_local().await? the compiler still complains about the recursive calls... but not with the f temporary.

If I combine the two lines such that they say boxed_local().await? the compiler still complains about the recursive calls... but not with the f temporary.

I'm a bit surprised by that too. It's probably because the compiler has a rather simplistic check for recursive async fn calls, and doesn't understand that foo().boxed_local().await is okay.

Even { let f = foo().boxed_local(); f }.await works fine, so it seems the compiler demands that the recursive call and await must be in different statements.

I hope this gets improved in the future...