Skip to content

Delay stabilizing async closures to consider if they should return impl IntoFuture instead of impl FutureΒ #135664

Closed as not planned
@eholk

Description

@eholk

Async closures are currently in beta for stabilization in 1.85. I'm very excited to see this feature stabilized!

That said, I would like the Lang team to consider delaying their stabilization to consider a new design detail. Delaying stabilization would amount to putting a PR in nightly that undoes the stabilization, and then doing a beta backport.

Note that this doesn't necessarily mean the lang team would be committing to making this change, but if the lang team feels there's a non-trivial chance they would prefer the IntoFuture semantics, then we should delay stabilization so there is time to consider this.

I talked with @tmandry and @traviscross about this and they did not remember discussing this particular detail during the stabilization process. If we are mistaken and this was discussed, then we should let the decision stand and not change anything.

Why should T-lang consider this change?

@yoshuawuyts recently published a blog post about generators and auto trait leakage, which argues that gen {} should return impl IntoIterator instead of impl Iterator. The key example in the post is (adapted slightly):

let iter = gen {
    let rc = Rc::new(...);
    yield 12u32;
    rc.do_something();
};

spawn(|| {
    for num in iter {
        println!("{num}");
    }
}).unwrap();

If gen {} returns impl Iterator, as it does on nightly today, this example fails to compile because the Rc is not Send. However, this is overly restrictive because the Rc is not created until the spawned thread calls next() the first time.

Instead, having gen {} return impl IntoIterator allows this to work, because the IntoIterator part could be Send, and once it becomes an Iterator it would no longer be Send.

This has a similar shape to annoyances we've seen with async Rust, where it's not uncommon for people to want to write futures that are Send until they are polled for the first time. If we had made async {} blocks return impl IntoFuture instead, this would be straightforward to do for the same reasons.

So then the next logical question is what should generator closures or async closures do? For much the same reason, it would be nice for async closures to return futures that are Send until first polled.

At this point, changing the return type for async {} would require an edition change. However, we have a brief window where we can still make this change for async closures.

Why should T-lang keep the status quo?

A lot of care has gone into the current design by @compiler-errors and others to come up with something that balances a lot of competing goals. For example, with the current design, the following example works, but it would break with this proposed change:

fn foo<F, U>(_f: F)
where
    F: Fn() -> U,
    U: Future
{}

// can be invoked with legacy, fake async closures or new, real async closures
foo(|| async {});
foo(async || {});

This is a desirable property, since a lot of existing ecosystem code is written using Fn() -> impl Future as an approximation of an async closure. If async closures returned IntoFuture then they would not work until existing code is updated to support async closures natively.

I bring this up mainly to show that it's not obvious that we actually should make this change. There are many factors in tension and there's a good chance we already have the optimal balance. We'd want to thoroughly consider any changes, so this issue is mostly about whether we want to take the time to consider those changes.

Mitigating Factors

This would still be changeable over an edition if we were to decide it's worthwhile but don't want to churn things now. If we were to make this change, we'd likely want to change the behavior of async {}. Doing both over an edition at the same time would keep the overall language more consistent.

Metadata

Metadata

Assignees

No one assigned

    Labels

    C-discussionCategory: Discussion or questions that doesn't represent real issues.T-langRelevant to the language team

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions