Hacker News new | ask | show | jobs
by hirrolot 1482 days ago
> without touching async

The pain comes from async. Over time, I came up to this conclusion: if someone tells me that Rust is nice and you only need to change your mind, this person doesn't write async code. Or he/she writes very-very straightforward, if not primitive async code and doesn't touch HOFs, traits, and similar stuff at all.

However, when you write networking code, you typically use async. The worst role is being an async library author (me).

7 comments

People talk about C++ suffering from its commitment to zero-cost abstraction, but the same thing applies to Rust async. While async may theoretically be the fastest possible way to write asynchronous code, it feels like an order of magnitude more painful than the CSP/channel-based approached used in languages like Go and Clojure (and the upcoming Java Loom).

Personally if I had to write async code that required anything other than the absolute minimum possible latency, I'd prefer to write Go, and I say that as someone who thinks Go's lack of generics was an absolutely terrible idea.

I made the mistake of trying to learn Rust while doing async programming.

IMO, when it comes to concurrent, it's a matter of picking your poison:

Threaded Rust: No overhead of a GC, but overhead of context switches and multiple stacks.

NodeJS: No overhead of context switches and multiple stacks, but the overhead of a highly optimized GC. (And I suspect that the GC can do tricks like run when the process is waiting on all tasks.)

Some real data would be very interesting.

Unless you really need the best performance possible there're pretty good alternatives. Mainly C# or Go and soon JVM with Loom.

Some numbers: https://web-frameworks-benchmark.netlify.app/result?l=rust,g...

Why the need to wait for loom when there is Kotlin and coroutines?
> No overhead of a GC, but overhead of context switches and multiple stacks.

Is it really an overhead if it happens in parallel? You have real parallelism, not only concurrency.

Only if your application can limit the number of threads to the number of physical cores.

IE, of you're doing a web server with a thread or process for each incoming web request, you're blocking and context switching. If you have to have locks, you're also blocking and context switching.

This is why async programming models are common, they move the logic of blocking and context switching into the language and runtime, where the compiler can juggle more concurrent tasks in a single thread. It's just harder to do in Rust because, to oversimplify, things that are in stack memory in a threaded environment are now on the heap. In C#/NodeJS, this difference is transparent, but in Rust it's not.

There’s plenty of real world data. I forget the name of the website but web server benchmarks are pretty good at showing the differences.
Yes those are the ones I was thinking of, thanks!
Async in something like C# is much less painful precisely because it doesn't try to be a zero-cost (or at least as low cost as possible) abstraction. When the language can allocate stack frames on the heap implicitly as needed, and there's GC to clean them up, things "just work".
Go now has generics. The performance of using them is hit or miss it seems.

Your opinion is valid, but I would say, if you aren't juicing for the best performance, you can adopt easier patterns to async. There are comments on this post detailing how to go about doing that. Or yea use another language if you want.

>you can adopt easier patterns to async

Can I do this without having to wrap half the libraries in the ecosystem if I want to use them without worrying about async? C# has that issue: the ecosystem buys heavily into async, so it can be hard to avoid it.

Probably depends on what you are using and trying to do. So I'll say 'maybe'.
But Go still doesn't have enuns with data like in Rust.
On embededded, you can write straightforward asynchronous code without using *Async*. You do it using DMA, and interrupts, perhaps with static analysis etc. There are efforts to use Async on embedded rust to abstract these, but it's not required.

Of note re networking: My observation is that the Rust Async ecosystem only covers TCP and higher. There are loads of Async TCP, HTTP etc libs, but nothing that can do anything lower than that! At that point, you're looking at perhaps `socket2`, and `smoltcp`; the latter, of note, also works on embedded, and goes lower than TCP, despite its name.

My best guess is that a lot of people are using Rust for TCP and HTTP level web programming, eg servers, where spawning 100s or more IO-bound processes at once makes sense; the area where Async shines. Why I'm confounded: #1: Rust excels at low-level programming; ie it's one of a select group capable of this (Along with C, C++, ADA, and zig) #2: Web application programming in Rust has a long way to go to get to the level of Django and Rails. It only has Flask analogues.

Neither of those goals are served by the existing Async-based ecosystem; it occupies a spot in between.

> The pain comes from async. Over time, I came up to this conclusion: if someone tells me that Rust is nice and you only need to change your mind, this person doesn't write async code.

Async code is a pain in almost any language. Certainly any language that differentiates between async code and non-async code has the async code be a pain.

> Certainly any language that differentiates between async code and non-async code has the async code be a pain.

Function colouring is not the only problem with async code. The difference is that concurrency in other high-level languages usually don't break down polymorphism and other language features. Also, they don't push you to deal with lifetimes, which is a serious issue in Rusty async.

Writing async code in C# is a lot easier to me than in Rust. Unfortunately, I didn't have a chance to write async in functional languages, such as Haskell or F#, because they are well-known for elegant concurrency.

I'm not sure if C# async was derived from F#, but it definitely looks very similar. The main difference is that in F# async is vastly more customizable (but also slower, because the compiler can't make certain assumptions due to said customizability.

https://docs.microsoft.com/en-us/dotnet/fsharp/language-refe...

Writing this kind of async code in Haskell (and to some extent OCaml) is much nicer, because you can abstract over the asyncness of code. This can't be done in Rust or C# because the type system isn't powerful enough (no higher-kinded types). To be fair, adding HKTs to Rust's existing type system is a challenging theoretical problem in itself.
C++ surely has HKT, that is what templates that take other templates as type parameters are for.
In a sense, but the "ill-formed, no diagnostic required" hack allows for scenarios where what you wrote is nonsense, and a human can explain why, but your C++ compiler doesn't have that insight, so it compiles anyway and does... something. This avoids needing to teach the machine how to determine if what you did was sound.

But this is of course not a very safe way to write software.

If the author is fool enough to not use the language features that exist since C++17 to validate template code, surely.

I also don't find debugging Rust macros that fun, yet most likely the answer will be that the macro author didn't took enough care, and Rust is great to write gigantic DSL macros.

> If the author is fool enough to not use the language features that exist since C++17 to validate template code, surely.

To be sure the fact the diagnostics aren't required does not forbid them from being provided, but it does mean you'd need to know whether you've been provided with such diagnostics and how effective they actually are. Unless the answer is "I have diagnostics and they are 100% effective" you're in the same situation.

> I also don't find debugging Rust macros that fun

Which kind? I don't find debugging the declarative macros too hard, they are after all just expanding what you wrote according to some simple rules, and you can ask the compiler to show that expansion to you.

Procedural macros present unlimited potential for exciting debugging because now you're essentially modifying the compiler at runtime. A C++ pre-processor macro can cause some nasty problems but it's not going to run a different compiler... [Technically Mara's nightly-crimes only runs the same compiler with different flags, but it could run a different one if she'd needed to do that]

Sure, but the exchange is that you get worse performance.
No, C# or Typescript is way easier, as a GC does the memory management for us. Lifetimes in async Rust are one the hardest things.
You might be right on this. I have written a lot of rust and a fair bit of network code, but I never went near async. I'm not exactly sure what my problem with it is, but I started writing rust before async was a thing and always felt more comfortable with finite state machines and polling for network code (I guess I am atypical).

Anyway, I am sorry that you have to use async.

There are two things to mention:

1. Async for trivial things is straightforward and easy

2. Rust actively discourages some async patterns to protect you from some memory misuse edge cases

It does limit your freedom in writing code that eg. relies a lot on async callbacks, but there's a reason. The first time I did a massive async project (think a rust binary maxing all cores executing the largest possible number of async fns doing various things in parallel - from fetching data online to running tensorflow) I came at it all wrong and wrote something that would have worked in node or haskell but that was a pain to compile in rust.

After days of pain I understood what rust wanted and nowadays I use the same pattern and it's fairly easy for me. Just another tool in the shed.

Did you do a write-up on this pattern? I'm finding myself in a similar situation lately and I'd appreciate the help a lot.
I disagree. I write Rust, I write mainly async Rust and while my code is maybe not the most sophisticated I use a lot of async traits and generics. I still find it one of the best languages I know, definitely my favourite for all around coding at the moment.
That's probably true. I've experimented a bit with tokio for local IO bound tasks, but decided it's unnecessarily complex. I think some patterns will emerge from current Rust async development eventually. Good parts will be easier and bad parts will be removed, but digestion of such concepts usually require time.