What is the future goal of async Rust?

I don't really understand how the coroutine (suspend/resume) part of it works, but Zig's async allows writing color-independent code. It uses a global mode for async vs sync, which is something not everyone likes and is still being debated. At the system IO function level, the mode is checked to determine whether to call a sync or async IO function. Zig is still a work in progress and in the current release the async feature has been temporarily disabled. There is also an accepted proposal to change the design somewhat.

I mention this mainly to point out that it is very difficult to accomplish async "color blindness" and a lot of debate about whether it is the right thing or not, and whether it requires giving up too much flexibility. Since the Zig async project is still in progress, I think it remains to be seen whether it really can be done well or not. Hopefully the end result will be good, and other languages can learn from it. In any case, Rust has not gone in this direction, so in that sense it's a moot point.

That is a "moot point". Moot Point: Definition and Examples | Grammarly Blog

1 Like

That has not been my experience:

  • Generic code can be hard to read.
  • Integer literals aren't generic enough, so constants are more annoying to construct (e.g. T::try_from(15_u8).unwrap().
  • Slices must be indexed with usize.

How is it relevant to our discussion, then? It's not hard to do async if you make your whole program async. That was already said in the very beginning.

So instead of two function colors it has two program colors. Like CPython vs JPython, I guess.

Yes, it may work, and if someone would create a fork of Rust without sync it would, probably, work.

So far I'm not seeing anything that wasn't done in Go or many other languages.

Indeed. Rust bases it's async support on coroutines, not runtime-based magic. These, too, are not new (Modula-2 used them for concurency did them 45 years ago), but they lead to entirely different set of trade-offs.

I would have much preferred if Rust would have just exposed coroutines instead of playing marketing games with async/await keywords, but I have to appreciate Rust developers persistently cunning approach: take solid math which actually works (but with Joe Average would never accept because hey, it's math, it's scary, we don't do math!) and then hide it behind buzzwords which are feeling warm and fuzzy.

I certainly wouldn't be able to do that, thus I appreciate what they are doing, I just wish there would have been some explanation of that they are doing without that thick veil of buzzwords.

2 Likes

I think what you want to say is that writing function of one, single, color is easier than writing universal function. Well, duh. Why is it suprising?

When I meant that there are “an easy answer” I meant that there an obvious way to create “function of universal color”. I haven't implied that it's as easy to write as non-generic function.

Yes, ergonomics of generics is, sometimes, problematic, but they do exist and you can use them to avoid copy-paste.

Nothing like that exists for making universal async/async functions (yet?).

I can't help thinking that if I had to use generics to paper over the difference between sync and async my code would end up looking like line noise and be far harder to read than what we have already.

Perhaps someone could write an example of what they mean by this for us?

There are examples in the keyword generics progress update @jameseb7 shared above.

1 Like

If I have understood it correctly, the Crab Elders have moved away from the syntax in that blog post and are instead opting for using effects instead. (I can't say I was very happy about the initial keyword generics proposal -- I, personally, prefer the effects system).

3 Likes

To be honest the idea of keyword generics terrifies me. Like generics and macros it's as if one is layering a language (or languages) that is not Rust on top of Rust in order to generate Rust. If you see what I mean.

It could end up in the tangled swamp of complexity that we see growing every year in C++.

None of it is really necessary. After all we can already we can write everything we need in Rust, from embedded systems to compilers.

Indeed, I only linked the keyword generics blog post above to indicate that there is some movement on how to improve async Rust. I haven't done any async Rust, but from what I have seen, it makes sense in terms of how it fits together, and I don't think there is much to be gained from hiding the difference between sync and async.

Some of the more immediate improvements seem to be from moving more async stuff into the standard library. As always, there's a reluctance to put too much into the standard library, so I don't think a full executor like tokio would be added, but a simple "run this async function to completion" function might be nice (something like the example given for std::task::Wake, or the pollster crate)). Some more combinators for futures would be useful to have in the standard library as well. All of this is already available in crates though, so it shouldn't inhibit development in the present.

It's not really about what can be expressed; it's about ergonomics -- specifically relating to DRY.

As someone who has a few crates that contain both async and non-async implementations for the same functionality, I think this is a worthy goal. With that said:

  • As I stated earlier in the thread, I'm not a fan of the original keyword generics proposal.
  • In my personal experience it's somewhat common that the async and non-async implementation diverge so much that it's questionable that keyword generics would have any significant benefit. (That is not to say I haven't had a few "optimal" cases as well -- it's just that those have been rare (maybe there's generally more implementation overlap with const fn?)).

If you're worried about superfluous sugaring of the language, don't fret: There are people arguing for the opposite as well. Not long ago someone on Zulip even argued that async fn is unnecessary, and may be detrimental in some cases.

1 Like

Hmm...

Firstly I don't believe DRY is a principal we should blindly enforce at every opportunity. Often it is better to say what you mean, here and now, rather than refer to where you said it earlier in complicated ways.

Secondly I don't know of any ergonomics of generics or macros that is pleasant to use. It's aways another layer of syntactic noise and complexity over what one its trying to express. It had better have a really good justification for its existence and use.

1 Like

The colour-agnostic benefits are aimed primarily for crate maintainers. For applications, I agree with you that DRY should not be followed religiously.

4 Likes

You're not seeing it at from the perspective of someone who needs to maintain crates that, for instance, implement some trait over all base integer types, which is commonly accomplished using macros.

You could obviously argue that there should be a better way to do that than use macros, but currently that's the tool we have (sans writing out every instance, which would be a maintainability nightmare).

1 Like

Ah, now there is a thing. The idea that library authors, who are obviously gurus in the language they are using, end up writing horrendously complex and unfathomable code that lowly application writers need not trouble their minds with. Effectively library authors use a different language than us mere mortals.

We see this to the extreme in C++. The C++ standard library is incomprehensible to most C++ users.

I liken it to something like Javascript or Python where the users of the language are not expected to know or care how all the features of their language and library API's provided are actually written in a different language, typically C++.

Does it really need to be like that?

1 Like

I agree 100% with you and I share your concerns, but I don't think they apply here. We are talking about such specific edge cases (crates with a generic API for different function colours) that I'm up for the extra parsing time when reading the code as an application developer if this eases the maintenance costs for crate maintainers.

3 Likes

If you lurk around on the rust-lang Zulip, you'll discover something pretty fascinating. Among the Crab Elders you'll find some of those who are responsible for the core features of Rust, and who can write unfathomably complicated macros and trait bounds ... and some of them, pretty staunchly, hold the same views as you on this.

You'll find them writing things like "Even I struggle to parse this -- what chance would a beginner have?"; the people I previously would have suspected would always argue in favor of a more powerful language/stdlib, are arguing for less power/flexibility in favor of readability for beginners.

I suspect the Crab Elders have weighed the pros and cons of these features, and determined that the value they add outweigh their added cognitive load.

3 Likes

This thread has gotten way too long and I want to first admit that I TLDR'd :confused:

But allow me to butt in here for a second and ask this -- Why do I, as a lowly application developer, need to be bothered with the .await keyword? I imagine that w/e your program is doing, there's only 4 possible cases:

  1. You're calling a synchronous function from a synchronous function.
  2. You're calling a synchronous function from an asynchronous function.
  3. You're calling an asynchronous function from a synchronous function.
  4. You're calling an asynchronous function from an asynchronous function.

In each of these scenarios, there's an exactly one scenario could possibly happen:

  1. Self-explanatory.
  2. The parent (the async function) would just not yield control back to the scheduler.
  3. The parent (the sync function) need to block on the async func until its execution completes.
  4. Self-explanatory.

Am I missing something there? Why do I need to explicitly add .await or call block_on on async functions? In the worst case scenario, I wrongly use an async function in a sync context and I fail to get the benefits, or I call a sync function in an async context and monopolizes the CPU, but what's wrong with that? Maybe I'm misunderstanding something obvious?

I guess I'm a bit confused here after @Alice 's explanation. I'm under the impression that because Rust wants to support execution environments that don't allow for non-async, the colored function analogy applies and people have to learn how to use async Rust.

Assuming that's the case, why can't the compiler infer what the execution environment is and automatically make everything async or non-async?

I'm mainly referring to @Alice 's comment in this other thread:

In general, the reason that async/await works the way it does is that Rust is that Rust wants to be able to work in settings where everything being async is not acceptable. Therefore async has to be explicit in your code, as opposed to just having everything be implicitly async like what Go does.'