An Alternative Syntax for Async Functions

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.

5 Likes

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".

4 Likes

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() -> _ {} over fn 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 normal fn

'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 asyncronous functions,

Note that the method isn't marked as async , because it doesn't need to be. The async 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 fixable 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 fixed 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: To await or not to await? - wg-async-foundations.

1 Like

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.

1 Like

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
}
1 Like

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.)

5 Likes

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

1 Like

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 :slight_smile:

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.)

9 Likes

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.

1 Like

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.