After the related discussion in Idea of how to bound types that async methods return, I decided to flesh out my thoughts in writing. I'd appreciate any thoughts or feedback.
In fact, the only actual problem I see here is people wanting async fn
to be exact syntax for asynchrony in traits... Also, the syntax feels long.
As of async functions - I don't think we want to change this: it's stable and it works.
You think async functions in traits should be different than regular async functions syntax-wise?
I think most people will disagree on the "is it too late" point, but I'll just note my disagreement and continue.
My biggest remaining note: in this version of async
, what is the value of async fn foo() -> _ {}
over fn foo() -> _ {async{}}
? If it's 'all_inputs
-- why not just provide 'all_inputs
for normal fn
? If it's error messages, why not just enable those for a function returning a tail position async
?
Note that there's also the idea for async
blocks, try
blocks to support a syntax
fn foo() -> RetTy = async {
}
which I think delivers all the benefits you're potentially claiming, except being "the simple way to write async
".
Not that they should, but they definitely can.
I think most people will disagree on the "is it too late" point, but I'll just note my disagreement and continue.
I doubt the change will happen as well, but I thought I'd put it out there after all the talk about implicit await being added over an edition.
what is the value of
async fn foo() -> _ {}
overfn foo() -> _ {async{}}
The value is saving the extra indentation, reducing the change from the current version, and staying similar to other languages.
why not just provide
'all_inputs
for normalfn
'input
would probably apply to all functions, not just async
ones.
Note that there's also the idea for
async
blocks,try
blocks to support a syntax
That is interesting, I have not heard of that idea before.
One other big thing of note:
By your own admission, for trait async
ronous functions,
Note that the method isn't marked as
async
, because it doesn't need to be. Theasync
modifier can be used when implementing the trait:
So... why can't that just be the case with the current async
syntax as well?
trait Foo {
type FooFut: impl Future<Output = usize> + Send;
fn foo(&self) -> Self::FooFut;
}
impl Foo for Bar {
async fn foo(&self) -> usize {
baz().await;
1
}
}
There's no reason this couldn't apply to current async
syntax as well, so it's not so much an argument for not "hiding" the existential impl Future
as it is an argument for allowing to write async fn
to implement trait functions which return an existential impl Future
.
but I thought I'd put it out there after all the talk about implicit await being added over and edition.
I don't think anyone actually proposed this? I brought up implicit await
again once as a "what if" for comparison purposes, but only to support the potential of using an explicit async
for a spitballed async
overloading feature.
It's an art (unfortunately) to tell what's being brought up as serious "the language should consider changing to be like" versus "this was discussed previously and the lessons learned then should be remembered" versus "here's a deliberately too-far proposal to compare against." I think all mentions of implicit await
post the .await
decision fall in the latter two camps.
Oh also, this wouldn't be possible as an edition change under current edition guidelines. One of the guidelines, in addition to "should be automatically cargo fix
able in the case without macros," we have roughly "warning free code in all previous editions (with the migration lint group on) should compile cleanly in the new edition" and "it should be reasonable to write code that compiles warning free in all editions (not using edition-exclusive behavior, and/or using k#
/r#
)".
With an edition change to your async fn
, there's no async fn
that compiles correctly on both editions, so the "reasonable" state that would be cargo fix
ed to is in fact just fn foo() -> _ {async{}}
.
cargo fix
IIRC doesn't also update the edition as well; it just applies always-machine-applicable error/warning suggested fixes to your code, so whatever it fixes to must work in the edition that is being checked / migrated from.
It's really appealing to say "editions let us change whatever to fix things," but actually making changes beyond keyword reservations, changing defaults, or "it's technically breaking, but we could still potentially rationalize it," is going to be a very hard sell if not impossible to push through.
I don't think anyone actually proposed this?
The async working-group has been discussing it: wg-async-foundations is now wg-async.
Sure, that's a possibility. The post is just my view on how things would work best, I'm not against picking and choosing parts of it that work well without breaking changes. I do think that making that the primary syntax would be much more consistent all around. It would be weird for users to do it one way for standalone/inherent async fns, and have to do it another way for traits.
Side-note on this code:
(ignoring the incorrect additional impl
)
I think you’d need something like
trait Foo {
type FooFut<'a>: Future<Output = usize> + Send + 'a
where
Self: 'a;
fn foo(&self) -> Self::FooFut<'_>;
}
impl Foo for Bar {
async fn foo(&self) -> usize {
baz().await;
1
}
}
to make this work properly, because the future returned by an async fn
captures the argument.
That's true, although I believe the + 'a
is inferred, and there's discussion about whether the Self: 'a
should be.
Suggested syntax is very clean and self-explanatory even to an untrained eye.
Could some next-best variant become the primary syntax
relegating async fn
to deprecated status?
fn foo() -> impl Future<Output = usize> => async {
1
}
Alternatively, async fn foo() -> usize
could be a warning instead of a hard error on older editions.
Woah, where/when did that happen? I missed it. (And I think it's an absolutely horrible idea – at least I'd like to voice my dissent wherever it was discussed.)
Based on a Carl Lerche post.
However, I want to emphasize that this is in the "Unresolved Questions and Controversies" section of the wg-async documentation, and while it may have been discussed further in Zulip (I didn't check), it hasn't been discussed further in the repository beyond documentation.
I believe this would be the most relevant Zulip thread: rust-lang Zulip #wg-async-foundations: Dropping .await
- what else could or would change
Yes, it was discussed on zulip after Carl Lerche's post. There was no serious proposal or anything, but it was mentioned that this is something that could potentially, hypothetically change:
Just wondering, is this something that could actually change? It would be a pretty huge breaking change, no?
We could certainly change it over an edition It would be a big migration though
If you only care about bounds, add return
as a production in the type syntax so you can write e.g.
await fn foo() -> T
where
return: Send,
i32: LolIdk<return>,
(Compare Self
.)
This should have the same expressive power without stepping on anyone's toes.
(I am, in general, a big fan of using return
in type position for this sort of thing.)
How would you conditionally bound the output of an async trait method?
trait Foo {
type Fut: Future<Output = usize>;
async fn foo() -> Self::Fut;
}
fn call_foo<F>(foo: F) where F: Foo<Fut: Send> { ... }
instead of return
it should be Return
(breaking change!) similar to how self
and Self
behave so you could have
fn foo(f: impl Fn() -> i32) -> Vec<Box<dyn Fn() -> i32>> {
let mut vec = Return::default();
vec.push(f);
vec
}
not sure how useful in this instance, but more consistent.
At first glance, this could look like return: Send
refers to i32
. The function signature says that the function returns an i32
after all. Most beginners to async would probably assume that.