There are certain features it’s missing, but nothing about the design really prevents them from being added. And many have been in the past few years.
I’d point to the “leakpocalypse” as a more fundamental Rust flaw.
There are certain features it’s missing, but nothing about the design really prevents them from being added. And many have been in the past few years.
I’d point to the “leakpocalypse” as a more fundamental Rust flaw.
Python, for example, continues to have similar issues, whereas Node/JS definitely did stick the landing, and Swift arguably did too.
Explicit yield / resume, like Lua? Full-on CPS?
Every time I bring this up there are people who jump in with excuses for why Rust can't do something similar. But these are either (1) historical accidents in the sense of implementing goroutines would've broken existing code [fair], or (2) excuses. Go's approach is much harder to get right, and much more pervasive in the impact on the language. But when it's done well, it's really smooth to use.
I use Rust as my daily driver because I want the safety guarantees that Rust gives, and cargo is pretty nice. But every time I need to do something highly concurrent, I wish I was working in Go...
It's almost as if there's two fundamentally different types of functions that people might want to declare, necessitating two function colors.
Go is a language that is primarily used for async programming, so the design of having everything async makes sense. Rust is a language that is primarily used for sync programming, async programming is catered for but it's not the focus of the language. So having everything be async doesn't make any sense at all.
Since any function can get indefinitely stuck in a system call, the main difference between "sync" and "async" isn't whether you can pause execution, it's who can pause execution. And the main downside to the more flexible option is that it requires extra overhead in how the stack gets allocated.
But why should the flexible/inflexible choice be made on a per-function basis? I feel like I'm manually annotating which functions will inline, except worse because certain combinations won't compile. Instead, how about having the compiler figure it out on a per-call basis?
The way the async feature works in Rust is that the asynchronous function is just a syntax sugar that gets desugared into a state machine struct during compilation. The way this state machine works is similar to how one could achieve async in a language like C. It's unfair to dismiss everything as excuses given that the fundamental aim of the language is different.
In Rust async functions are not really colored because again - the async function is just a syntax sugar for a struct you can create and use in a sync context. The colors analogy is only really applicable in a language like JavaScript, where there's no way to poll an async function in a sync context.
FYI you can't poll an async result in a sync context in Rust, either.
Sure you can. Async runtimes in Rust are written in Rust, that's exactly what they do.
First, it would be a huge undertaking. That in itself is a huge time/resource burden.
Second, it would add overhead to any non-async function call. Because async introduces branching on every function invocation, it would make the resulting assembly even harder to understand. This strongly goes against the zero overhead/ zero cost abstraction idea of Rust.
By the same measure, Go could technically remove (almost) all GC, add some kind of borrowing mechanism and steal Rust's thunder.
Generally speaking, applications tend to either (1) exist as low-level shims or embedded code that doesn't use coroutines, (2) be large Rust projects that don't call out to non-Rust stuff often (e.g. a web server), or (3) large performance-sensitive applications that call low-level APIs frequently (e.g. games). The above solutions handle all three of these cases.
What Rust chose instead was to optimize for a compilation mode which supports all three cases at once, with zero cost for FFI. This sounds nice, until you realize that what they traded off to get this was UX. Writing concurrency in Rust is lightyears better than C++, but it still sucks compared with Go or Erlang.
But I don’t think it would be the right move to make Rust something that it’s not, just so people don’t have to think about what a Future is or isn’t.
The whole point of higher level abstractions is making it so that developers don't have to think about stuff. It's what I love about Rust: with the borrow checker, I know that if it compiles then I am not going to be accessing mutable or already freed state. The language and the compiler get more complicated so that I don't have to think about stuff.
Rust didn't go that way with concurrency. They instead opted to push that complexity (thinking about what a Future is or isn't) onto the developer. YOU have to think about "should this API call be async?" instead of letting the downstream user decide that.
Whenever necessary I have been able to do this easily. Calling async code from a synchronous context is not an example of something which can’t be done in Rust today.
> Or just make all functions asynchronous but eliminate the state machine if it contains no yields.
The optimizer generally already cleans this up.
> The whole point of higher level abstractions is making it so that developers don't have to think about stuff. It's what I love about Rust: with the borrow checker, I know that if it compiles then I am not going to be accessing mutable or already freed state. The language and the compiler get more complicated so that I don't have to think about stuff.
The borrow checker is not “limited cost”. It is zero cost.
> Rust didn't go that way with concurrency. They instead opted to push that complexity (thinking about what a Future is or isn't) onto the developer. YOU have to think about "should this API call be async?" instead of letting the downstream user decide that.
There is no zero cost way of doing this.
Once you’ve exceeded its capabilities, you have the choice to either pay a runtime cost (use RefCell and friends) or deal with maintaining and validating unsafe code.
To the point that even disliking Go, I might favour it over Rust, unless using any kind of automatic resource management is forbidden, and I don't feel like reaching out to C++ for whatever reason.
I could imagine another language where all “functions” are Futures, but I don’t think that would be the right move for Rust.
At the very least, that alternative language would probably have to provide a default executor. IMO, that’s a bit much for a language that aims to have a very minimal runtime.
Yes, it would have to provide a default executor in the standard library. And it would have to have a noasync mode (analogous to nostd) for embedded applications that can't have asynchronous execution. These are hurdles, but not very large ones.
The ship has sailed though.
I would really not prefer a Rust that includes a more intrusive runtime. Especially given the API changes things like io-uring may require to exploit.
As it works now, I can nearly always predict what assembly I’ll see when I compile a Rust program. It’s predictable enough to work anywhere C does. I would hate to lose that.
> embedded applications that can't have asynchronous execution
Is most definitely not the case.
They can't have the same type of async runtime that would be optimal for a web server or the likes (and I'm not sure all desktop applications and web servers are going to always benefit from the same runtime in the same way), but that's a point in favour of Rust's model imho
If you're interested this is an embedded async runtime that's expected to run in no-std and no-alloc environments
not
"embedded applications can't have asynchronous execution"
there are embedded applications that are compiled with no concurrency runtime. I'm not saying all embedded applications work this way.
The point about a runtime optimized to run bare metal on a single core probably being suboptimal on a desktop machine with 8 cores still stands though. (And the laptop one is possibly not going to work optimally on a server with hundreds of core either)
You shouldn't await in an interrupt handler, but there's a lot of things you shouldn't do in an interrupt handler. A rule like that doesn't mean you need a noasync mode. (And if you really wanted to use await there, you could make it work with priorities or with a separate task queue.)
Oh,rly?
Swift mitigates a lot of the function coloring pain: synchronous methods can conform to asynchronous protocol requirements, thanks to the compiler generating thunks (at least, I think that’s how it works)