rfcs icon indicating copy to clipboard operation
rfcs copied to clipboard

`async T` and `gen T` types

Open joshtriplett opened this issue 1 year ago • 11 comments

Allow the syntax async T and gen T as types, equivalent to impl Future<Output = T> and impl Iterator<Item = T> respectively. Accept them anywhere impl Trait can appear.

This RFC was inspired by a few different needs.

First, writing large numbers of functions that manipulate iterators or futures. Having a shorthand for the type makes function signatures much clearer.

And second, providing one part of a general solution that gives people the benefits of async fn in all contexts, and for new constructs like gen.

Co-authored-by: Eric Holk

Rendered

joshtriplett avatar May 07 '24 08:05 joshtriplett

An alternative I'd like to see explored is impl Future -> T and impl Iterator yields T. Definitely more wordy, but also less opaque, more extensible – and generalizing the existing syntax for Fn traits.

tmandry avatar May 07 '24 23:05 tmandry

An alternative I'd like to see explored is impl Future -> T and impl Iterator yields T. Definitely more wordy, but also less opaque, more extensible – and generalizing the existing syntax for Fn traits.

That would open the conversation of extending that syntax to all traits with only one associated type. Something like impl Trait<T>

oriongonza avatar May 08 '24 19:05 oriongonza

@eholk and I discussed some points related to this draft, and I know that he's working alongside joshtriplett on incorporating them. As these same items have come up in discussions with others, though, I'll go ahead and mention them here to reduce duplication:

  • In argument position (and perhaps in some places in bounds), we might consider whether this should desugar into impl IntoFuture/impl IntoIterator instead of impl Future/impl Iterator. Many, probably most, functions would be better served accepting the Into* variants. We wouldn't want to push people away from doing the right thing here by making it much more comparatively verbose to write that.
  • We might consider removing gen T from this RFC and adding it instead as a future possibility (or alternatively, adding an unresolved question for which trait to use in the desugaring for gen T). Since we haven't yet worked out the situation with respect to pinning and self-referential gen blocks, it's not yet clear that gen should exactly equal Iterator. As we did in RFC 3513, we probably want to leave space here.
  • We might want to discuss the placement of use<..> (RFC 3617) in this syntax, e.g. async use<..> T.
  • We might want to discuss the subtle relationship between async T in this syntax and the pending syntax for async traits, e.g. async FnMut() -> ().
  • We might want to discuss whether this syntax, combined with doing async traits, would foreclose the possibility of supporting bare trait syntax (but desugaring it to impl Trait rather than to dyn Trait as was done in earlier editions). It's not clear whether or not we'd ever want to do this, but the possibility had been left open.
  • As @tmandry mentioned, @eholk has an alternate proposal that he described in this post for impl IntoFuture -> u8 syntax (and generalizations, including supporting this syntax for arbitrary traits). It would be interesting to consider the relative merits of that along with the proposal here.

In talking this over with @yoshuawuyts, we pondered this question also:

  • This RFC is proposing async T as syntax for an opaque future type. If we were to do this, would we want some analogously impl-eliding syntax for an opaque future closure type, i.e. for impl async Fn/*{Once,Mut}/*() -> T (i.e. the output of an async |x| { .. } block), or for an opaque generator closure type?

traviscross avatar May 08 '24 21:05 traviscross

This reminds me of the old complaint about TAIT that it means that type G<T> = impl Iterator<Item = T>; doesn't work in a copy-paste way for APIT.

Should there be a way that users can make their own aliases that work for RPIT and APIT? Or is this only useful for Iterator and Future?

Feels at least plausible that I'd want to make a

type[not_TAIT] Task<T> = impl Future<Output = T> + Send + 'static;

or something.

scottmcm avatar May 09 '24 03:05 scottmcm

Should there be a way that users can make their own aliases that work for RPIT and APIT? Or is this only useful for Iterator and Future?

Feels at least plausible that I'd want to make a

type[not_TAIT] Task<T> = impl Future<Output = T> + Send + 'static;

or something.

for a pure syntactic construct you could use a macro :eyes:

macro_rules! Task {
    ($t:ty) => { 
        impl core::future::Future<Output = $t> + core::marker::Send + 'static
    } 
}

fn run(a: u8) -> Task!(u8) {
    async move { a }
}

fn flip(x: Task!(u8)) -> Task!(u8) {
    async move { !x.await }
}

kennytm avatar May 09 '24 11:05 kennytm

I think this will only add to the complication of using Rust.

Rust is already a pretty complex and verbose language and adding more sugar syntax is not the answer.

Now let's take a new Rust developer perspective. They have used some basic async and asked themself the question. Can I have a function handle the result of an async result?

If they try this

fn do_something<T>(value: async T) {}

What have they learned? Nothing. Async is still in their mind as a weird magical property that is added.

But currently, this is how they would learn to do it.

fn do_something<T>(value: impl Future<Output = T>) {}

This shows the developers. Oh. Async is just sugar syntax to the Future trait.

I don't think the syntax suggested in this RFC adds much benefit to the Rust developer experience and only complicates things.

An alternative I'd like to see explored is impl Future -> T and impl Iterator yields T. Definitely more wordy, but also less opaque, more extensible – and generalizing the existing syntax for Fn traits.

I enjoy how this syntax looks. However, I don't think this is necessary. This could be explored in something like this

trait Future {
	// I don't have a good name for the attribute
	#[some_marker]
	type Ouput 
}

This allows for the trait impl to be written in the suggested impl Future -> T The impl Iterator yields T would be impl Iterator -> T But even this I feel like this is unnecessary sugar syntax and does not solve the problems that face Rust developers.

Overall my opinion is that Rust does not need more syntax and more ways to do one thing.

wyatt-herkamp avatar May 09 '24 18:05 wyatt-herkamp

What have they learned? Nothing. Async is still in their mind as a weird magical property that is added.

But currently, this is how they would learn to do it.

fn do_something<T>(value: impl Future<Output = T>) {}

This shows the developers. Oh. Async is just sugar syntax to the Future trait.

just wanted to point out that impl Future<Output = T> is itself syntactic sugar. Do you think they also learned nothing about what impl is?

Not to say I disagree with your assessment that Rust doesn't need more syntax at this point. But the argument to be made here should be why we draw the line at asynx T, not that syntactic sugar makes things "magical" -- that is a given, and a necessary tradeoff.

Wyverald avatar May 09 '24 20:05 Wyverald

Is impl Future<Output = T>/impl Iterator<Item = T> really the desugaring we want in argument position? It would require the associated type to be exactly T, which is sometimes overly restrictive. For example, a function that accepts an impl Iterator<Item = &'a str> could probably handle an impl Iterator<Item = &'static str> just fine. So perhaps the argument-position desugaring should bound the associated type to be a subtype of T, instead of exactly T.

Jules-Bertholet avatar May 10 '24 00:05 Jules-Bertholet

just wanted to point out that impl Future<Output = T> is itself syntactic sugar. Do you think they also learned nothing about what impl is?

True. I think some syntactical sugar is good, however, with all things. It is good in moderation. Rust has a good amount of syntactic sugar. But continuing to add more will only make the language more complex

Not to say I disagree with your assessment that Rust doesn't need more syntax at this point. But the argument to be made here should be why we draw the line at asynx T, not that syntactic sugar makes things "magical" -- that is a given, and a necessary tradeoff.

Personally, I don't see an overall benefit to async T. It seems like an overall unnecessary feature compared to other features the Rust team is working on. I would much rather see more progress made with async working group or stabilization of other trait-related features such as type_alias_impl_trait.

wyatt-herkamp avatar May 10 '24 16:05 wyatt-herkamp

@Jules-Bertholet extending the type system with the ability to require subtyping is a significantly more involved undertaking than a simple async Ty desugaring. It should not be done as a "quirk" of async Ty and instead a principled extension to the type system that can be applied to other applicable cases, such as the Fn* family of traits.

Ad hoc special cases like the one you are proposing are the kind of thing that wind up with inadequately specified behaviour and causing soundness bugs or future compatibility problems, all of which cause problems with the long term development of the type system.

BoxyUwU avatar Jul 19 '24 19:07 BoxyUwU

No disagreement with @BoxyUwU that such an extension should be principled. I think it's worth taking @Jules-Bertholet's suggestion as a future possibility and thinking about whether it can be done backward-compatibly, or if not, whether we might want to save this syntax for then.

tmandry avatar Jul 19 '24 22:07 tmandry