Sep 28 2023

Was async fn a mistake?

This stabilization PR for async fn in traits made me think: was async fn in Rust a mistake?

I mean, I dunno. Maybe it wasn’t. But play along for a moment.

By the way, I don’t mean that async/await in Rust itself is a mistake. That’s a Big Deal. It allows companies to deploy some serious stuff to production. And async and await syntax is a huge save. I don’t want to lose that. Writing manual futures and poll functions is megasad.

I’m specifically talking about the async fn sugar. What if we didn’t have it, and instead just returned impl Futures, and used async blocks inside the functions?1

The current async fn is really nice, if you fit the expected usage. If none of the differences with impl Future ever cause you problems, then great! But I do run into them. Other people seem to also.

What’s so bad?

Some of these differences cause problems that don’t have decent solutions. (Do you know the differences?)2 If you have to deal with one of them, suddenly you need to use different syntax.

And now, people need to understand both. And keep the subtle differences in their head when they read. Does that make things better? Or worse?

It’s the only place that has a magic return type. It makes lifetimes weird. With suggestions to reign them in. It leads to all sort of proposals about how to customize the return type. #[require_send], async(Send), Service::call(): Send, and I’m sure there’s others.3 I also am thinking about generators and streams, since they could also end up with magic return values.

So was it mistake? I think it may have been. Don’t worry, I don’t want to take it away from you, if you disagree!4

What if the alternative was nicer?

But I did wonder about this. What if we had the following features ready:

  • Repurpose bare trait syntax to mean impl Trait. It’s been enough editions, right?
  • Ability to forgo naming an associated type name.
  • Stealing the feature from Scala where functions can equal a single expression.

Then asynchronous functions could look like this:

fn call(&self, req: Request) -> Future<Response> = async {
    // ...
}

That’d be a nice improvement.

  1. Yea, I know, it’s a little more writing. But I am in the optimize-for-reading camp. We read much more than we write. So if I have to write a few more characters at a function definition, but it makes the reading experience more understandable, that’s a massive win. 

  2. I’ve been involved in async Rust since the beginning. I know how it used to be, I was part of the group making it better, and I pay close attention to all the new proposals. I still mean what I said: none of the solutions look nice. 

  3. Return Type Notation (RTN) syntax is probably the least gross. But it raises a bunch of questions. Does it work for all functions? If not, why not? If so, do I check I::Iter or I::into_iter(). And also to consider: Rust’s strangeness budget

  4. I could see an argument that it’s sort of like for, while, and loop. A more convenient syntax when it works, and you can use the others when you need more control. That argument breaks down when async fn is part of a trait definition. But anyways, I really just want the less-sugared way to be little nicer. 

  • #rust
  • #opinion
  • #async