-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
async T
and gen T
types
#3628
base: master
Are you sure you want to change the base?
async T
and gen T
types
#3628
Conversation
Co-authored-by: Eric Holk <[email protected]>
[explanation]: #explanation | ||
|
||
In any context where you can write an `impl Trait` type, you can write | ||
`async T`, which desugars to `impl Future<Output = T>`: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this won't work when lifetime needs to be captured right?
use std::future::Future;
async fn g(a: &str) -> usize {
a.len()
}
fn h(a: &str) -> impl Future<Output = usize> + '_ {
// ^~~~
async { a.len() }
}
I suppose #3617 will need to be a prerequisite in any more complex situations assuming you don't want to utter the impl
word.
fn i1(a: &str) -> use<'_> async usize {
// ^~~~~~~
async { a.len() }
}
fn i2(a: &str) -> use<> async usize {
// ^~~~~
async { 0 }
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would expect that in any context where you could write impl Future<Output = usize> + '_
, you could write async usize + '_
. You could also use use
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we write async async async async usize + '_
the lifetime attach to the outermost async
?
Type | Desugared |
---|---|
async async usize + 'a |
impl Future<Output = impl Future<Output = usize>> + 'a (maybe require async (async usize) + 'a , like &(dyn Trait + 'a) ) |
async (async usize + 'a) |
impl Future<Output = impl Future<Output = usize> + 'a> |
async impl Send + 'a |
impl Future<Output = impl Send> + 'a |
async (impl Send + 'a) |
impl Future<Output = impl Send + 'a> |
Since that + '_
is part of the TypeParamBounds syntax does this mean async usize + Send
is allowed 🤔
An alternative I'd like to see explored is |
That would open the conversation of extending that syntax to all traits with only one associated type. |
@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 talking this over with @yoshuawuyts, we pondered this question also:
|
This reminds me of the old complaint about TAIT that it means that 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. |
[prior-art]: #prior-art | ||
|
||
We have special syntaxes for arrays in types, `[T]` and `[T; N]`, which are | ||
evocative of the corresponding value syntax for arrays. Similarly, the syntax |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TBH, I think that array types are horrible precedent that exist primarily because there weren't const generics in 1.0. Array<T, N>
would be completely fine, and avoid the weird ;
in there that's unlike anything we have anywhere else in Rust. [0; N]
is similarly bad, since it's trivially-but-disastrously typo'd as [0, N]
, and has lead to still-unresolved questions like [0; _]
that wouldn't exist if it was just repeat(0)
(turbofished as needed). And unusual syntax makes it less obvious that it can be nested and a higher edit distance to change to some other type instead. (For example, Array<T, N>
→ ArrayVec<T, N>
is nice, [T; N]
→ ArrayVec<T; N>
is a pain.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@scottmcm I think even if we had perfect const generics, we'd still want the shorthand array syntax because it's evocative of arrays.
The use of `async fn` to hide the asynchronous type serves as a partial | ||
precedent for this: the case made at the time was that users cared about the | ||
output type of the future more than they cared about the `Future` trait. This | ||
RFC extends that benefit to any place a type can appear. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has also introduced the send bound problem, however. I think this RFC should talk about how impl Send + Future<Output = T>
fits into a hypothetical async T
.
obfuscating the difference between associated types and generic parameters. | ||
|
||
# Prior art | ||
[prior-art]: #prior-art |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The prior art that jumps out to me, but is currently unmentioned in the RFC, is impl Fn() -> T
, since that actually does involve associated types.
If this RFC doesn't want something like Task<T>
because it
may confuse users by obfuscating the difference between associated types and generic parameters
then I think the more direct path would be to a shorthand that is evocative of one that constrains an associated type, not something that looks closer to a generic parameter where you just dropped the <>
s.
for a pure syntactic construct you could use a macro 👀 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 }
} |
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.
I enjoy how this syntax looks. However, I don't think this is necessary. trait Future {
// I don't have a good name for the attribute
#[some_marker]
type Ouput
} This allows for the Overall my opinion is that Rust does not need more syntax and more ways to do one thing. |
just wanted to point out that 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 |
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
Personally, I don't see an overall benefit to |
@Jules-Bertholet extending the type system with the ability to require subtyping is a significantly more involved undertaking than a simple 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. |
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. |
[summary]: #summary | ||
|
||
Allow the syntax `async T` and `gen T` as types, equivalent to | ||
`impl Future<Output = T>` and `impl Iterator<Item = T>` respectively. Accept |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there particular motivation for desugaring to impl
rather than desugaring to the trait and allowing the user to specify impl
vs. dyn
? The current approach would preclude users doing things like:
fn takes_iterator(iter: &mut dyn gen T) { ... }`
fn returns_future() -> Pin<Box<dyn async T>> { ... }
Allowing dyn
would make this syntax usable in more places and would help mitigate the existing "cliff" users fall off of when transitioning to dynamic dispatch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is, I'm imagining that gen T
would be sugar for Iterator<Item = T>
, and async T
would be sugar for Future<Item = T>
.
Allow the syntax
async T
andgen T
as types, equivalent toimpl Future<Output = T>
andimpl Iterator<Item = T>
respectively. Acceptthem 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 likegen
.Co-authored-by: Eric Holk
Rendered