810 comments

[ 4.3 ms ] story [ 364 ms ] thread
Rust isn't hard. Programming is hard. Rust just points out failure states before you encounter them in production.
All type systems will have meaningful and true propositions which are apparent to the programmer but not yet to the language team. Choosing a typed language is choosing to have a relationship with a living & evolving team/ecosystem, one which can improve over time in their ability to express and prove propositions or provide ergonomic abstractions.

Some of what the author is complaining about matches my conversations with people who aspire to be Rust library authors — that you're often trying to hijack the type system because you <do> actually know better.

I enjoy Rust, but it's not that simple. It really is more work to accomplish certain tasks in Rust than many other languages even when what you're doing is safe.

The narrative that "Rust isn't hard" is getting tiresome, and I say this as someone who writes a lot of Rust. Let's be honest that Rust can be harder than many other programming languages in many ways, but those of us who use it believe the upsides and tradeoffs are worth the minor to moderate increase in difficulty.

Pretending Rust is easy just sets beginners up for disappointment when they get into Rust and realize it wasn't what they were sold. Or worse, they start doubting themselves when they encounter the hard parts because Rust fans were busy insisting it's all easy.

Writing widely useful, performant, reusable, correct and stable libraries is very hard. Rust is the easiest language to do that in.

If someone says programming in another language is easier, it's because they are not attempting (or are failing) to do one or more of those things. Those things are not always important so that's fine, but a lot of programmers (myself included) have this dream of being able to solve a problem once rather than over and over again, and that's why Rust appeals to us, and why we argue against Rust being hard - because it's actually easier for our particular usecase, but clearly not everyone has that same usecase.

> Writing widely useful, performant, reusable, correct and stable libraries is very hard.

Correct.

> Rust is the easiest language to do that in.

Wrong.

Rust provides very substantially less support to library designers than C++ does. Anyone creating ambitious libraries finds Rust a big step down.

Rust might get more of what C++ offers library designers as it matures, but only at the cost of whatever simplicity it can still claim.

> Rust provides very substantially less support to library designers than C++ does. Anyone creating ambitious libraries finds Rust a big step down.

Before I used Rust my main language was C++ where I specialized in writing libraries, and this is a ridiculous statement.

It's only recently that the C++ standard library has gained enough functionality to do even some basic things in a portable way, so you're relying on other libraries to provide that, yet there is no standardized way to declare dependencies on those other libraries. There is no module system to ease structuring library code. There is no hygiene - a ton of stuff you include will just pollute the global namespace. There is no standard way to version your code. There is no standard way to update a library. Everyone uses incompatible string types - seriously, if you think Rust has too many string types, wait until you find out that every freaking C++ library represents strings differently, sometimes using the same types though! There is no standard place to publish libraries. Even basic language types like `int` differ massively from platform to platform, or even between different compilers on the same platform. Each compiler's preprocessor behaves slightly differently. The programmer must manually forward declare their functions, types, etc, and the rules are different for inline/templated code. All code is unsafe, and yet the rules for what constitutes UB are informal at best. (Whereas in Rust, the rules for UB are also not fully defined yet, but this is only relevant for the minority of your code which is not safe). All experienced C++ programmers think in terms of lifetimes, and yet cannot express this through the type system, so this must be documented informally. There is no standardized coding style or format.

I'm going to stop now just because I'm bored but this list could go on for a very long time...

What makes UB in C++ is spelled out in its International Standard; there is no specification for Rust, just an implementation. It has been many years since C++ preprocessors differed notably from one implementation to the next. Nothing in C++ is global except what you choose to make global.

In fact today C++ lifetimes are expressed in the type system, and this is an example of what C++ enables a library to provide.

If, working as a library designer, Rust was not a big step down, you were neglecting to provide users of your libraries much of the value you could have offered. Your remarks suggest that libraries you delivered were closer to C than C++.

> What makes UB in C++ is spelled out in its International Standard

That's not true, the standard only specifies that some things are UB, it is not exhaustive, nor is it unambiguous. Furthermore, no current C++ compiler is compliant even with those parts of the standard which everyone agrees on (eg. see proposals to introduce a "bytes" type to LLVM to resolve known miscompilations). There is work to improve this, such as defining an explicit memory model, but Rust is ahead of C++ on this.

> In fact today C++ lifetimes are expressed in the type system, and this is an example of what C++ enables a library to provide.

Do you have an example of this, or are you talking about using smart pointers? Smart pointers are about ownership, not lifetimes.

> Your remarks suggest that libraries you delivered were closer to C than C++.

Most of my remarks were about consuming other libraries from my library, which is not something I have control over. Sure, I can use smart pointers and other modern C++ features in the API I expose... That doesn't change any of the points I mentioned.

I'm in the same boat, I spend a bit of my working day writing in Rust .. it's a great language but it's very cumbersome and I find it suffers form poor ergonomics, like it's annoying to type
I find it enjoyable to type for the most part. Explicit type casts aren't a treasure or anything, but then I think about why I am doing it and I can't really complain.

I feel you, but, my experience differs in the end I guess. To be fair turbofishes we're something I discovered by intuition, so maybe I'm a lost soul.

Rust isn't hard for the things you try to solve in Rust.

The author compares Rust code with Go code. Those two languages serve entirely different purposes, with entirely different mechanisms behind it.

Go does what the author does with ease because the runtime fixes all the complicated parts for you. You tell Go what you want and it'll try to solve all the memory management/threading/memory safety issues for you, usually succeeding.

Rust doesn't do that. Rust expects you not just to tell it what you want, but also how you want it to happen.

I think comparisons between Rust and Go/C#/Java are what will really trip up a beginner. Rust has a lot of nice features found in higher level languages, but it's decidedly not a higher level language. Rust operates in the space of C and C++, where a small mistake can cause memory corruption no debugger will ever be able to unravel, but where a well placed byte of padding can accelerate a program by as much as 30 percent.

I think the difficulty in Rust lies in that it will enforce correctness. Competing languages are less strict about that, especially when it comes to threading. You tell them a piece of memory is safe to use across thread boundaries and they'll believe you, and most of the time you can rely on race conditions not screwing you over perfectly well. A C program can be short, fast, and clear, as long as you leave out the error checking and resource management in case of failures; with Rust you often don't get that luxury. Writing correct code is a slow, tedious, painful experience, and in Rust you'll have to live with that pain (unless you throw around unsafe{} everywhere).

I believe that teaching programming should follow a bottom-up approach, but many others disagree. If you've dabbled in assembly, done some multi threaded C(++) and experienced the challenges in low-level code, Rust should be enjoyable enough to learn after cursing at the borrow checker a bunch of times. If you're a top-down learner, though, you'll run face-first into low-level problems and their complexities for seemingly no good reason.

> I think the difficulty in Rust lies in that it will enforce correctness. Competing languages are less strict about that, especially when it comes to threading.

Enforcing correctness at compile time is not the only way to insure correctness.

Some do enjoy solving language puzzles (so choose Rust) and some prefer thinking before coding and prefer solving design puzzles. I personally prefer that latter, as the 'hard' problems are intellectually interesting, solving them is satisfying, and over the years the design lessons build upon each other. At which point you don't need a Mommy Dearest Compiler to ensure correctness.

This is the "don't do anything wrong" model of software development, and while it works well for some, we have enough experience as an industry to know it doesn't scale.

Crucially, it's hard to prove whether or not you've actually solved whatever design issue you wanted to overcome. Such a proof usually would entail some sort of analysis of the program as written (because it may actually differ from your design). To perform this analysis, you may want to annotate the lifetimes of the various objects as they are declared, so that you can track (for example) that some memory is not accessed after it is freed, or any other number of issues.

This lifetime analysis as you would imagine can be very tedious and complicated, so you would perhaps want to automate the process. And that's essentially why Rust's borrow checker exists. It's almost inevitable that it should exist imo. Seems completely obvious after the fact.

To quote threatofrain:

> All type systems will have meaningful and true propositions which are apparent to the programmer but not yet to the language team... Some of what the author is complaining about matches my conversations with people who aspire to be Rust library authors — that you're often trying to hijack the type system because you <do> actually know better.

Rust catches some problems (like data races and use-after-free). Safe Rust translated into correct C++ is still correct. Rust also fails to catch some problems (like preventing out-of-bounds indexing at compile time); admittedly idiomatic C++ fails to catch bounds errors at runtime. And when encountering problems that Safe Rust cannot solve (like the generic lifetime quagmires in the original post), C++ often makes it possible (and easier than esoteric programming languages like Unsafe Rust) to solve the problem correctly in the current situation; though admittedly, ensuring you haven't missed any UB cases, and validating that your assumptions don't break later on, is difficult (Unsafe Rust is better at marking unsafe code for future readers).

> Enforcing correctness at compile time is not the only way to insure correctness.

> some prefer thinking before coding and prefer solving design puzzles

Yeah, you just need to guarantee that person working on it, considered all edge cases, had uninterrupted time to think, thought about how the edge cases interact, didn't make a single mistake, wasn't sleepy, under influence of substances, and perfectly wrote it into the program without a single semantic error (off by 1).

Easy.

That's why I code in Malbolge Lisp CodeGen that outputs Brainfuck.

If the benchmark is "projects that are known for idiomatic C", such as Redis or Sqlite, we know that even they introduce memory corruption errors that lead to vulnerabilities every now and then. You're not better than them.
And yet those C and C++ projects continue to be very popular and successful. :-)
If you can solve all your problems by thinking before writing code then you will never see a compiler error, unless you see a compiler bug. If you're solving your problems beforehand by thinking about them but end up solving language puzzles, you clearly haven't thought enough.
In fairness, for the kind of software that C++ is particularly suited, the idiomatic software architecture is thread-per-core, which has the distinction of being almost entirely single-threaded at the code level. Race conditions aren't a meaningful concern because data virtually never crosses thread boundaries. The bigger issue, particularly and mostly for C code, is object lifetime management.

If maximum performance, whence thread-per-core architectures originate, is not the objective, then GC languages start to become more attractive and C++ may not be the right tool for the job. And in those cases, Rust may not be either.

You're not wrong, but the crux of the problem described in the article is that Rust's object lifetimes are very hard to get right (even for the people working on the compiler) when working with cross-thread code.

I'm no professional Rust dev but I wouldn't have written the code like this; I know Rust isn't particularly suited for this style of callback mechanism and I know not to try and force this paradigm into Rust the same way.

For example, I think the author would have had a much raiser time if instead of passing async futures, they'd use channels or some other message passing mechanism in combination with a bunch of blocking threads to communicate events. Such a mechanism would also translate into Go quite easily (less so for other languages, though).

This example was deliberately picked to show a complex problem with writing Rust. I don't think this represents a challenge you'd face very commonly if you were programming Rust all day, at all. It's not bad criticism, but it appears to imply a much wider problem than there really is, in my opinion.

I don't really know what kind of programs require such an elaborate callback system commonly enough where it even makes sense to use Rust. C#, Java, and Go are fast, easy to write, and each have libraries to do almost anything you want. That 10-30% speed boost you can achieve with well-written Rust is probably not really worth the effort, especially with upcoming AOT compilation features in C#.

Rust isn't a solution to all problems, and neither is any other programming language.

>I think comparisons between Rust and Go/C#/Java are what will really trip up a beginner. Rust has a lot of nice features found in higher level languages, but it's decidedly not a higher level language. Rust operates in the space of C and C++, where a small mistake can cause memory corruption no debugger will ever be able to unravel, but where a well placed byte of padding can accelerate a program by as much as 30 percent.

I agree. But if you use Rust for web programming, it is fair to compare it with C# or Java. On the other hand, C and C++ feels easier to read and write, a bit more productive than Rust.

So the question is vs C# and Java: is the performance worth the pain and loss of productivity?

And vs C and C++: is the guarantees made by Rust worth the pain and loss of productivity?

I'd argue that in some cases the answer can be yes, while in others it ca be no. So, there's no universal good or bad choice, it really depends on project, team, budget and many more.

I don't know why you'd possibly want to use Rust for web programming, to be honest. When you add a full stack of databases and entities, Rust barely becomes faster than ASP.NET or Spring Boot. I messed with it for fun, but I don't think I'd pick Rust as a web server language any time soon.

The only reason I can think of is the WASM space, which Rust lends itself very well to, to reuse the same entities and data structures in the front end. Then again, you'll end up writing a terribly bloated web UI and other languages have similar bindings.

I think for new projects where C++ makes sense, Rust probably makes more sense. There are some edge cases (if you expect to be operating on trees in memory, for example, or if you're interfacing with libraries written in other languages) but I think Rust is generally better for such system tools. That assumes that you have in house Rust devs, of course; if you're a C++ shop, you'll have to teach everyone a new language before the switch makes sense.

The C(++) crowd is difficult to teach other languages because they, more than any dev group I've encountered, seem to have a larger amount of vocal people who think their code is perfect, they won't ever produce bugs, and all those compiler errors warning about failing edge cases are unnecessary because they know best.

> Pretending Rust is easy just sets beginners up for disappointment when they get into Rust and realize it wasn't what they were sold. Or worse, they start doubting themselves when they encounter the hard parts because Rust fans were busy insisting it's all easy.

You could be describing me. I recently convinced my boss to let me write a server in Rust for the safety, speed, etc. After being two weeks overdue, I threw it all out and wrote a working version in modern C++17 in an afternoon. Of course part of the issue was language familiarity, but I think also what I was trying to do was objectively harder in Rust in many respects, and the ecosystem of crates was less mature than battle-tested set of libraries I was using in C++.

At the end of the day I want to ship code and move on to the next project. Rust wasn't helping me there.

Which libraries did you use in C++?
Boring things like boost, abseil, and the standard library.
Other languages make this massively simpler by just disabling possible sources of failure like multithreading.
Im not so sure. There are a lot of error states that won't happen in production ever with other languages because the language/libaries handle those conditions gracefully without you needing to care. Rust pushes this to the edge, _which is good if you want that contol_, but is a cost you have to pay.
>Rust isn't hard. Programming is hard.

Programming should be hard only if the problem you are trying to solve is hard. Creating an boring CRUD app shouldn't be hard. Predicting with a good degree of success the market trends for stocks and options should be hard.

This right here. Rust is a low-level language hat makes a whole dimension of implicit knowledge explicit.

This is a very good thing, but if you are a programmer that is used to "copy paste, and then it works". You will have a very, very, very bad time with it. Rust forces you to think about memory. In an age where dynamic typing is so prevalent this seems like a fading art.

Why do you think the vast majority of software is written in GC languages? How does Rust point out deadlocks?
Rust is hard until it's not!
I expected this to be more one-sided - and it was a little ranty - but it also made a lot of good and substantial points
Unpopular opinion: I find rust code unreadable. And I consider myself a polyglot. I am sure all those symbols have a special meaning but it's like perl to me. Just throw some random special characters and they all mean something.
Perl did nothing wrong, but I echo the sentiment you have about Rust.

Last year I ported a mid size internal project to it as part of a resolution to learn a new language and the experience left me feeling like it was just me who "didnt get it". Building things took a lot longer, not just in the amount of code, but also the compile time. Ultimately compile time was the dealbreaker. On more than one ocassion I'd have to go back to the git history to remember what it was I had planned to do next.

Rust pro tip: don't compile til you're really done. Use something like cargo check or rust-analyzer to quickly spot errors without wasting time on the rest of compilation.
Thanks for the tip
You are not the only one. I find rust code unreadable too even though I can understand it. I wish the type signatures and function definitions were on separate lines a la Haskell. That would have drastically improved the legibility.
You can do that in Rust by using a "where" clause. Put in a letter for each type, then define them on different lines.
Anything with trait bounds beyond `fn foo<T: Trait>(t: T)` should put them in the `where` clause (and in this case, it arguably should be `fn foo(t: impl Trait)`).
I completely agree. I think the syntax looks awful, for one. This has put me off learning it completely.
I had that same issue but got over it once I started diving into rust. What really helped me are the compiler error messages. Without that i would have given up learning it
It's unreadable until you learn the language. It is not something you can just look at and start writing/reading.

Also there aren't that many special symbols, it's not scala after all.

Spend two weeks with it and it makes sense why things are the way they are for the most part.

I find it readable, but I do sympathize. I would have like to have seen it look more like an ML language such as OCaml or F#.
Nothing a couple of days can't fix. Unless you have single-unpaired-quote-phobia.
I find Rust easier to read in a real IDE with semantic highlighting (although the IntellIJ plugin constantly breaks this) and inlay type hints so that everything doesn't end up blurring together.
My only gripe of Rust's syntax is the lifetime annotation, but I can't suggest any better alternative, either.

> And I consider myself a polyglot.

Then you should realize that Rust borrows some of its syntax from other languages:

- Function's parameter and return types are borrowed from Python's type annotations.

- Turbofish syntax is from C++.

- Closure syntax follows Ruby's.

I've been programming in Rust for the last few years. My daily work is mostly just fumbling through compiler errors until the code works.

Some observations:

- The compiler is always right.

- Do what clippy and rust-analyzer says. Don't ask questions.

- If you're fighting the compiler, clippy, AND rust-analyzer, then you're almost definitely wrong.

Virgins try to use &str everywhere. Chads just use String.

This sounds like a terrible way to exist.
Happy way to exist!
I suppose ignorance is bliss after all - I mainly hope you are exaggerating when you say 'fumbling through'.
Getting paid to mechanically crank through compiler errors? Sounds okay actually!
I wonder what approach acts as the best mentor for someone who wants to eventually develop a good mental model for how systems work. The C approach where you suffer at runtime and then have to debug ferociously, or the Rust approach where your mentor hits you with a stick all day?
I had C programming experience before touching Rust. I may be biased, but I think this is the right way to understand how computer systems work. Otherwise, Rust's design choices will make little sense to you.
I wish there was a Rust-like programming language that was just a little bit higher level than Rust. I like Rust's wide ecosystem with high quality packages, the nice type system, traits, the idea that my code generally runs pretty fast even if I'm being lazy about writing good code, and my code usually working correctly if it compiles.

I care about speed and correctness but Rust makes me also care about ownership and lifetimes even when I don't really want to care about that stuff. I've written Rust for years now and this still occasionally can really slow me down. Rust is still a very nice language so I just deal with it :) no pain no gain.

I would love a Rust-with-GC or something in that spirit that is similar to Rust but automatically figures out lifetimes as much as possible and GCs whatever it cannot figure out automatically. I've looked at languages like Nim and read about research languages like Koka so I'm optimistic something will emerge from this space I really like.

> I care about speed and correctness but Rust makes me also care about ownership and lifetimes even when I don't really want to care about that stuff.

I don't think one can care about speed and correctness without caring about lifetimes and ownership. Even if you do not care about memory ownership and use garbage collection, there are plethora of other things that you take care of. For example, if you've juts sent an RPC/HTTP request, and a user closed a particular window, should you abort the request or not? What if the user have closed all windows except one? Can they actually close window X without doing something in window Y first? What should happen if you forgot to abort the request and now the callback is called, but the original caller is gone?

I feel this fact is grossly underappreciated. Caring about speed and correctness but not ownership and lifetimes (and thread safety and race conditions and...) is like caring about road safety but not caring about headlights. You're not avoiding ownership and lifetime problems, you're just avoiding looking at them.
Thread safety, and memory model safety, and all this important stuff, is important! But ownership and lifetimes are models of reality that Rust asserts as part of its domain language, which, like all models, are approximations of reality, not reality itself. They're useful to the extent that they help to solve a given problem. And not all problems fit into the assumptions that they assert.
I think after the (correct) first sentence you are a bit mistaken here. The example you describe can be solved correctly and also with good performance in garbage collected languages such as Haskell or Scala. I would even say that those languages make a correct solution easier than in Rust, but they trade in a bit of performance for it (e.g. by using immutable datastructures instead of an ownership model). But for async stuff, I think this is actually simpler.

However, when performance is key, then Rust is by far the easiest solution to get it performant and still correct.

"good performance" is a relative term. You may consider Haskell or Scala having "good performance" but for others having garbage collection itself is a big no. So you are either left with Rust's approach where you limit the programmer, or C++'s approach where you leave memory management mostly to the programmer
I think you missed the point here.

My response was focussed on the part of the challenges of async logic. And what OP essentially said is that Rust does not only help performance because of the ownership model (vs. garbage collection), but also because it makes it easy to come up with correct and performant solution to async-related problems.

My point was that, when ignoring the performance benefits of ownership model vs. garbage collection, a language like Haskell or Scala makes a performant and correct solution for async problems easier than Rust does - at least at the current point in time.

I hope it's more clear now.

You can get great performance from garbage collected languages, to be honest. I guess what you can't get is perfectly predictable latency — but that only matters for a pretty narrow class of applications.
How then people write high-performance servers in JVM?
Sounds like OCaml is exactly what you want. Many of Rust's features are even inspired by it.
As well as syntactic constructs.

For example, Rust's unusual 'a syntax for lifetime parameters is Ocaml's syntax for generics because lifetimes are a special class of generics.

I think that's ocaml. Higher level, with GC, nice type system, no traits but signatures and higher order modules might be enough for you, and a compiler that produces fast native binaries.
I think you are pretty spot on here. I write Rust and Haskell most of the time, where the choice depends on the kind of project I'm working on. Also I've written Rust professionally but Haskell has been confined to random hobby projects.

I love Haskell but it has its own flavor of problems. I've meant to brush up a bit on OCaml; I've heard it has interesting features in its module system that Haskell does not have (maybe what you call "higher order modules").

Yup. Professional OCaml dev here, higher order modules are pretty cool. But OCaml shows its age and without typeclasses I don't find it very pleasant to program in. Most days I'd rather use Rust to be honest.
Yep ocaml seems to be a perfect balance of all the best of other languages.
There are plenty of languages to choose from if you allow a GC. I think Rust's niche is systems-level programming where a garbage collector is an impossibility. I wish there was an easier to use language with Rust's features which occupied this niche. I find myself reaching for modern C++ instead of Rust when I want to be productive :(
> I find myself reaching for modern C++ instead of Rust when I want to be productive :(

You are not the only one, this is also my experience.

And if you think about it: This is fine. There is a reason why we are not using TLA+ or format proofs framework when we want to do quick prototyping: Safety has a cost like everything else.

It can be a run-time cost (in case of ARC or GC languages) or a mental effort / productivity cost but it is still a cost.

Same here, I like Rust a lot, however in one side I already have JVM and .NET languages for 99% of the stuff I do, and for the remaining 1%, having C++ burned on my brain since 1994, with the 40 years of tooling, I rather spend the time using it.
Ocaml is just a mess. Has like two standard libs. You'll just fighting everything but your application logic instead.
There is only one OCaml build system (dune) which is used by people that don't write their own build system. But yes, you have the freedom of using alternative to the OCaml standard library if you wish so.
I might terminally dumb
Having an npm like ecosystem is hardly better.
Or F# if you want a wider ecosystem, it’s based on Ocaml and has many nice functional features, but you can utilize any .net code you want
Swift is by far the closest from a language point of view, but alas the ecosystem is all-but-nonexistent outside of the Apple systems.
This is a shame.

Swift could be a lovely general purpose language. I like the balance swift strikes between ease of use, expressiveness and performance. Like, swift has an equivalent of Option - but there’s syntax sugar for it. Rust has String / &str / Etc. To get a SSO string you need to pull in an external crate. Swift just has a built in, good, general purpose SSO string as part of the language.

But adding random half baked features to swift seems to be on the promotion path at Apple. Nothing is well documented. The language doesn’t feel stable and there isn’t much of a broad community like there is with rust, javascript and python. It’s a pity!

I would argue that swift is pretty distant compared to the ML family of languages - specifically Haskell. Besides garbage collection, the biggest difference between Haskell and Rust is that Haskell already has higher kinded types.
What about a transpiler that added a ^ operator that made a variable actually an Arc and inserted unwraps, clones, etc as necessary?
Go and Hare are rust-adjacent with just a touch of crayon involved. I highly recommend both especially Go since you mention rust with GC
I like Go a lot. At my previous employer a lot of the systems we wrote for log processing were written in Golang. And I thought it was really nice; I can compile my code really quickly and code in general will run pretty fast. I've only used in the space of big data file processing though.

My only real issue with Go is that I feel the language part itself is simplistic and doesn't have the kind of type/trait/typeclass/similarthing like Rust or Haskell has. In Haskell I especially like doing DSLs using monads, e.g. I did a integer-problem-to-SAT-problem DSL and another DSL for NetHack playing AI. In Go making these DSLs is more pain. Although I think that would be true for Rust as well, just not as much.

Just wacky I had to scroll this far to see Go mentioned. In certain contexts Rust is very impressive, in others Go is a much better choice. I'm really starting to hope that we'll see some of the important advancements in Rust packaged up in a better language.
Go is very deliberately designed for when you depend on a team with sharply limited skills.
These one-liners aren't super-constructive, my own comment was hardly in-depth, so whatever...

On any team or in a community your most likely to have a range of skill levels across members. A language can be many things, a powerful tool or a strong barrier.

When I first heard about Rust I thought we were on the verge of getting some real advancements across a much larger community of developers and scope of projects. Instead I think we're headed further down a path of at least 3 major groups of PL (scripting, memory-managed, precise semantics). Who knows, maybe it's for the best. If so we better get busy improving the inter-op.

It's interesting that you find Go to be Rust-adjacent. Setting aside the USPs of each language (goroutines and borrow checker respectively) and just speaking about the general experience of writing code, I find Go painful for all the reasons that I find Rust pleasant.

The best summary I can give of Rust is that it was designed by people who learned from the mistakes of the programming languages that came before it.

The best summary I can give of Go is to quote Rob Pike's response [0] to a request for syntax highlighting in the Go playground:

> Syntax highlighting is juvenile. When I was a child, I was taught arithmetic using colored rods (http://en.wikipedia.org/wiki/Cuisenaire_rods). I grew up and today I use monochromatic numerals.

[0] https://groups.google.com/g/golang-nuts/c/hJHCAaiL0so/m/kG3B...

It's like my favorite John Quincy Adams quote, "It is what it is, and it ain't what it ain't" Different strokes for different folks you know? At the end of the day if folks code at all, and it makes them happy, that's a good thing.
If your primary criticism of golang is that its creator doesn't like syntax highlighting, then it must be doing pretty good.
It is mostly an example of the mindset that I think GP is trying to illustrate.

Go has nil where Rust has Option.

Go has weird not-quite-tuple returns & if err != nil where Rust has Result.

Go has no real enum concept, where rust has its powerful enums and matching constructs.

Go has generics, but only after a decade of pressure from users (and even then, they are much much less useful than Rust's type system).

I like Go and I feel very productive in it, but its commitment to simplicity is dogmatic in many ways and it very much is missing milestone advancements in PLs from the past several decades. It could easily have been made in the 90s.

I would love a language that combined the best aspects of Go and F#: expressive, great type system, fast compilation, native binaries, opinionated formatter, excellent concurrency, comprehensive standard library, consistent documentation, etc.

Throw in great compile to JS capabilities with small bundle sizes and the ability to map Typescript typings into the language so you can seamlessly consume well typed libraries, and man. Killer language, right there.

Me too! Go's tooling and runtime are great, I just want a more useful type system.
I feel like Go was built with primary design considerations that are often not considered publicly and often at odds with what programmers want out of a language. I saw one of the first public talks from the creators at Google I/O and they stressed two things: compilation speed and new developers coming up to speed quickly. From what they said, Google had a few C++ projects with multi-hour compilation times that, when profiled, showed about 90% of the time was spent reading header files. So a core Go philosophy was single-pass compilation to cut down compile times as much as possible. Similarly they stressed that the focus on simplicity meant there wasn’t as much variation in style of Go code and new programmers—even those unfamiliar with Go—could quickly come up to speed and contribute to a project.

Viewed through this lens, the resistance on the part of the creators to changes that compromise these values even a little bit makes sense. Generics take time to get used to and any code base that makes extensive use of them will take longer to get up to speed in, even if it enables you to move faster later on.

That talk has really shaped how I look at Go. I think it solves problems that Google has (really large projects built by teams that have a ton of turnover) really well. But as with a lot of things that emerge from Google, it’s a solution to a problem that not too many other companies face. The ones that do will get an awesome tool that’s proven to work. But the ones for whom it’s 90% of what they need are going to get a lot of pushback getting that last 10% accepted because it already does almost exactly what Google needs it to do and any departure from that will be, in their minds, counterproductive.

I agree, I've mostly made peace with what Go is and I still enjoy using it.

It just tantalizes me because of how close it is to my ideal general-purpose programming language. There is a large middle ground between the minutes-long compile times of Rust and the seconds-long compile times of Go.

If such a compromised process produced something close to your ideal, that should worry you deeply.
As someone that writes a bunch of go but has never really gotten off the ground with rust:

  * Option? I don't mind if err != nil, but sure, it would simplify some things.
  * Result and real tuples? awesome.
  * real enums? sign me up.
If that was all added to go I wouldn't mind at all. But what does any of that have to do with

  impl<'a, H, Fut, Tail> Execute<'a> for Dispatcher<H, Tail>
  where
    H: Fn(&'a Update) -> Fut + Send + Sync + 'a,
    Fut: Future<Output = ()> + Send + 'a,
    Tail: Execute<'a> + Send + Sync + 'a,
  {
Shit like this makes my brain hurt
It doesn't, primarily the way I saw the discussion heading was (warning: strawmen ahead):

> "Go is a good alternative to Rust because it is easy to write performant, concurrent code"

> "Go is not very comparable to Rust. I won't describe why, here's a quote by Rob Pike and I'll strongly imply it's because its creators deliberately avoided complexity, even where useful."

> "You did not explicitly criticize anything about Go, therefore it must not have flaws"

Then I came in and described exactly where I feel Go ignores the state of the art in PLs.

> But what does any of that have to do with (Rust-shaped vomit)

Go doesn't need all those type system gymnastics because it does not have the problem of the borrow checker to deal with and doesn't promise to avoid segfaults for you.

It is a serialization of relative lifetimes, basically. Whenever you write code in any language that doesn't have a GC, you either have to maintain a mental map like this to avoid bugs, or you make the language do it for you like Rust does.
3 issues you listed with Go are actually about the same thing (the lack of sum types):

- Go has nil where Rust has Option.

- Go has weird not-quite-tuple returns & if err != nil where Rust has Result.

- Go has no real enum concept, where rust has its powerful enums and matching constructs.

And the point on generics coming late is unfair. All programming languages got major features introduced late (for example async/await in Rust).

But I admit I'd love sum types, a less verbose error handling (but to be honest I'm not sure exactly how), and that I missed generics.

Yeah I don't mean to criticize Go's generics for being less featureful given how late they were introduced, but they do currently prevent me from building any kind of mapping/filtering/pipeline style code because of their limitations (no generic type parameters on methods). A Go implementation of Result or Option could paper over the lack of sum types if we only had that.

The more I think about it, what I really want is something with the ML feeling of Rust, but in the space that Go occupies (good performance GC languages). Go frustrates me because it nails the runtime and tooling side of things but falls far short of it in the other ways I mentioned.

I absolutely agree with everything you just wrote and would use a language like that.
There's no shortage of good-perf languages with a GC: F# would be one prominent example that sounds very similar to what you describe.

The usual objection is that it requires a .NET runtime, whereas Go produces a single self-contained executable. But .NET can produce self-contained executables these days.

Yes, .NET can produce binary executables, but is it a common practice? I’m asking because I think the ecosystem matters a lot. If it’s a common practice, then it’s more tested and more stable and you get more tools and documentation.
It's a relatively recent feature, at least the "pure" implementation (what they had before was, essentially, a self-extracting binary), so it's still gaining popularity. But seems to be fairly common with containers.
As the sibling commentor said, syntax highlighting is only one example meant to illustrate Go's sometimes unnecessarily frustrating design. In case it wasn't clear, the example wasn't that Rob Pike doesn't like syntax highlighting. The example was that Go's playground doesn't have syntax highlighting (even as an option) for a reason as arbitrary as "because I said so".

That is why I find Rust and Go to be dissimilar. Rust is a language full of good ideas, almost all of which didn't originate in Rust. Go feels like it was designed with a very specific brief - "we want C but with GC and easy concurrency" - but whose designers otherwise had an NIH syndrome-like aversion to good ideas and common sense.

I don't agree with your framing. You are stating a 'moral' proposition, i.e. Rust == Good Ideas AND Go == Bad Ideas, and then applying the 'moral' position to the language as a whole. So you end up saying, in brief (and acknowledging that I do not think you are actually making a moral claim about the qualities of these languages) you are saying Rust is Good, Go is Bad and therefore they are dissimilar. I don't think the reasoning your putting forward actually says anything about the language and there similarity/dissimilarity.

I am not saying that Go and Rust are similar or not. I don't really agree with GP's comment where Go should be in the running for a Rust-with-GC/slightly-higher-level-Rust, but the frankly dismissive Rust Good therefore not similar to Go Bad is not justified. Although the conclusion regarding similarity is probably correct.

Also, this is a little off topic, but I have seen multiple people say that Go 'was designed with a very specific brief - "we want C but with GC and easy concurrency"' and then go on to complain about the lack of things like generics, destructuring match syntax, functional concepts, etc. But, all of those discussion, including your comment, start out with what seems to be an acknowledgement that Go's design had a very specific target. I agree that Go is basically the fulfillment of the brief you gave, i.e. C with GC and easy concurrency. So, when the target is C with two unique things, why does everyone then seem confused that Go doesn't have all of these extra 'good ideas and common sense'.

I'm not trying to turn this into a thread on Go's merits or lack thereof. I just don't understand why so many people seem to think that Go somehow didn't do exactly what it set out to do. You don't have to think what they decided to design was a worthwhile language, but to expect a lion to be a shark, or an apple be a steak, is just illogical.

I appreciate your comment. My intention wasn't "Go is dissimilar because it's bad and Rust is good" but I can see why you'd think that. I'll attempt to clarify.

The reason I like and recommend Rust is the number of decisions it gets right which are completely orthogonal to the borrow checker. It's clear that a lot of thought was put into the unexciting parts of the language. That's why I like using it even though I don't particularly need the borrow checker and I'd be happy with GC. Some examples off the top of my head:

  - Expression-oriented nature makes code easy to write and nice to read
  - Compilation errors are as clear and helpful as possible
  - Comprehensive yet skimmable API documentation
In short, Rust is a nice language outside of the borrow checker.

A lot of the things which strike me as nice about Rust can't be said about Go. For that reason I think Go is a surprising suggestion for someone who likes Rust but doesn't care about ownership and lifetimes.

There are a number of minor but valid frustrations I encounter when writing Go which are completely orthogonal to the brief of "C but with GC and easy concurrency". I'd understand if these problems were a result of the language's goals but often they seem to exist for no particular reason. In that regard I think Go is quite dissimilar to Rust.

I hope that explains my viewpoint more clearly :)

You’re comment make your point very well. Honestly, so did the first comment. Sometimes I find myself so tired of just lurking and reading HN that I just text dump some comment onto a totally valid parent comment.
Hare is not rust adjacent, sorry. It can't even represent the (small) stdlib of rust since it lacks generics, RAII... Not to mention the memory safety. It belongs in a different class of languages.
You are not correct but I'll accept your opinion as your understanding of it.
Go's lack of expressivity and weak as hell type system is hardly close to Rust, like I can barely see a similarity other than both being a PL. Hell, probably Java is closer to Rust than Go, at least it is also nominally typed.
Try D it has most features that you just described. It feels Phytonic and it's GC by default[1].

There is an ongoing work for borrow checker for D language, and pardon the pun but I believe D is borrowing just the right amount features from Cyclon and Rust for safer compiled software without overly complex programming syntax.

[1]Origins of the D programming language:

https://dl.acm.org/doi/abs/10.1145/3386323

Without diving into completely obscure programming languages, you can look at Scala, F#(doesn't scale super great ime), or I guess golang.

That said, they all don't give you what rust gives you and have their own troubles.

The truth is, either you care about life times and ownership, or you really don't care much about speed and correctness. Not saying that targeted at you as a person, but the royal "you". Even in c/c++ I have to think about that stuff, there's just no tools for it .

Fwiw you can import crates to have a gc in rust. To me it defeats the purpose. Wishing you the best on your search

What makes you think that F# doesn’t scale? Anything you can do in (say) C# you can do in F#?
I've bounced around a few languages this year, looking for a language to use in my spare time for fun / enjoyment / skills growth (I have a similar set of criteria to you). I've looked at rust, swift, haxe, zig, and now nim, and I think I'll be sticking with nim. I like that it's seemingly simple, and takes care of memory management for you, but you still have access to pointers if you want them and can extend the language with its macros. It seems like a language that I'll be able to grow with / pick up complexity as I want it, but by default I'll have concise readable code. What was your take on nim?
There were many things I liked about Nim:

* The compiled programs run pretty fast.

* Compiling is pretty fast too (compared to Rust or Haskell).

* It's easy for me to bind to whatever C library I have even if nobody made nice Nim packages for it. This one was important for the project I was working on.

* There is a GC, but if you choose the proper GC it will be based on reference counting (or if you are compiling to JavaScript, it'll use JavaScript's GC). It means I don't have to care about cleaning up resources. My memory may be wrong but I think Nim also tries to remove reference counting checks when it can. I haven't ever checked in the compiled code does it do a good job at this.

* The language rarely complains that something in my code is wrong; there's no borrow checker complaining. Even with little experience I was able to write some quite complicated code. It's like Nim wants to do its best to compile and run my crappy code.

* It's easy to read. Maybe because it looks so much like Python and I have lots of Python experience. Nim does not want to complain about your code unless it has to. Despite being statically typed, you don't have to write type definitions that much.

There are things I don't like as much:

* The story for running threads in Nim is not great. You can run OS-level threads but it's a bit janky (you'll have to now care what data can be shared). It wouldn't stop me from writing multi-threaded software though if I really needed threads.

* The documentation could be a bit better. I think Nim project should take this giant page: https://nim-lang.org/docs/manual.html and reorganize it and make sure the language is easy to read and find stuff. I often have trouble finding documentation on some language feature. I think the project is acknowledging this right in the first paragraph "This document is a draft! Several of Nim's features may need more precise wording. This manual is constantly evolving into a proper specification."

This one is harder to pinpoint into specific examples but the language feels a bit immature. I feel like there's bunch of half-baked features that were thrown in on a whim idea. And obviously the package ecosystem isn't as wide as in more established programming languages. I looked it up and Nim has apparently existed since 2008; I'm sure it used to be way more immature ;)

Despite the negatives, I think Nim is an amazing tinkerer's language. I can very quickly write programs that will be speedy and easy to read. I'm not sure I'd start a very complicated large project in Nim though. I am bullish that Nim will mature and its userbase will grow, fixing some of the warts.

In a few months I plan to take a long vacation and work on a video game with my friend. There is a high chance that I'm going to use Nim for this project; to make something that runs both in browser and also natively.

Good summary, I haven't touched threads but I'd agree with everything else. I'd also add that since the language feels dynamic, I feel like my code is less structured, but I think this might just be because all the boilerplate you have to write in other languages isn't there. So maybe my code is just as unstructured and with nim I can't be fooled by boilerplate into thinking it is.

I've been following the nature of code and making a little 2d platformer at the same time using nim + raylib. Compiling to wasm I found to be even simpler than when doing the same thing with C++. It's really nice being able to compile to native or wasm with a single compiler flag.

Try ocaml or F#. I did the same as you and was surprised how well balanced ocaml/F# are compared with everything else.
I know some folks are working on this but I just want cargo for JS
I really like the idea of something like vlang (https://vlang.io), as I've tried multiple times to find the motivation to learn Rust, but always end up going back to golang for the simplicity.
Unless there is some very compelling reason to need Rust, a lot of people would be better off with Golang or Vlang, because it would make their lives easier in terms of more general usage and ease of use.
> I've looked at languages like Nim and read about research languages like Koka

Nim is indeed going in the right direction with ORC/ARC

In addition to the Ocaml recommendations I suggest Haxe, which is basically "Ocaml concepts transposed into a compiler targeting various GC runtimes". (Compiler is also written in Ocaml - it's not kidding around) Easy to pick up if you already know JS syntax, and basically covers the "best-of" of static inferred type systems, but you can easily break out of it if you need dynamic or low-level behavior.

Downside is that it's not convenient if you just want one standard library and runtime because it targets all of them. You have to justify the trade-offs involved in that, but it's a good secret weapon.

Sounds like you're describing Swift. I'm trivializing it, but Swift is basically a higher level Rust with everything wrapped in an Arc.
As someone who does Swift 9-5 and dabbles with Rust, this statement rings true to me.
People don't like to mention C# since like PHP, it has a bad rep from early days, but C# 10 and .NET 6 hit all the sweet spots people are mentioning. C# and .NET now run on Linux and Mac and compile to a single executable like Go with tree shaking so the binary is much reduced. I don't really care, but it's a shame people don't take another look at C#. When I learned Rust I was surprised how similar it felt to C#, but much more ergonomic.
Static interfaces are still in preview, DUs aren't being actively worked on, that's what I miss from Rust when I'm writing C#.
Have you used the OneOf library? It's not DUs, but it's kind of an 80% solution in that space.
>I wish there was a Rust-like programming language that was just a little bit higher level than Rust.

I think F# might fit this role.

Would it be possible to do something like Rust+Go where you integrate Go code natively into Rust? So Go pointers become `GoPtr<T>` where `GoPtr<T>: Copy + GC` etc. Then all Go structs implement `GoStruct` which would have a static method called `reflect` that returns all the information required for type reflection. One thing I love about Rust is its genericity that at least makes imagining such things possible, though I do expect a lot of engineering effort.
D lang is incorporating a borrow-checking system. I'm not certain how far along they are with it. Walter Bright posts here often and will know. D has a wide range of GC and non-GC techniques that may meet your needs.
And no D programmer says "it was really hard but worth it in the end". It's worth it right from the start.
Elixir?
Not for system-level programming.
Well they want something GCd so it won’t be for systems programming anyways
Scala might just be that. It has a very strong type system which is quite similar to Rust's, lifetimes are managed by the JVM's state-of-the art GC, and all in all it is a very expressive language. Think of python-level expressivity, but all statically typed with type inference. It also has a similar stance on the functional-imperative question as Rust has - it prefers functional concepts but lets you write imperative code when you want to hand-optimize something.

Oh and I almost forget to point out that it can just use basically one of the largest ecosystems, and can also compile to js or native (for the latter there is scala native as well as graal)

Does it have a borrow checker and mutability control?
It is a GCd language, a borrow checker is very seldom needed outside of that (and frankly, one should just use try-with and similar constructs for other kinds of resources), so why would it need one? As for mutability, it is not enforced on a language level (only shallowly), but the standard library, the language primitives and basically everything makes control over mutability very good. In practice it won't be much different than Rust with the interior mutability pattern.
“Side-effect” of borrow-checking - there is always only one who can write to a variable. It's much more important than GC.
Oh I see. No it does not have it enforced at the language level, but it relies heavily on immutable data structures, and the type system is strong enough to express a complete actor-based concurrency library.

But since it is interoperable with Java and that exposes low-level primitives of concurrency, it can’t really be made guaranteed data-race free.

Your responses were quite informative, thank you. Even for me, Scala looks more interesting now. I have some Java adepts, I’ll try to convince them to climb a few steps.
> Rust-like programming language that was just a little bit higher level

I think the best answer is Scala. It has an awesome community/libraries/ecosystem. See this for example https://news.ycombinator.com/item?id=31601040#31604573

There are other great answers, like F#, Haskell or OCaml, but these are not as mainstream and that comes with its challenges.

With Scala you get and awesome IDE, surprisingly wide variety of libraries and the whole Java world as a backup.

Koka and Vale both seem to be decent (experimental) approaches to languages that learn from Rust, both what it got right, and what it got wrong.
I think Rust will eventually repeat the story of C++. 1) There will emerge more ergonomic languages that solve real-world problems that Rust attempted to solve. 2) Rust's issues with type system and async will be largely resolved, so it could be used in areas where it is absolutely required.
Pascal was almost there

It puts the reference counting in the type system. Like the default string type and arrays (=vectors) are reference counted. And for speed you could use manual memory management

Any string literal is like an arc<string>. E.g. in Delphi you can now write string concatenation like:

   var a = 'bcd';
   var b = 'xyz';
   var c = a + b;
which corresponds to Rust like

   let a = Arc::new(String::from("bcd"));
   let b = Arc::new(String::from("xyz"));
   let c = format!("{}{}", a,b)
I think the big thing that hurt Pascal initially was the poor start it got off to. By the time things like Borland Pascal came around and fixed the weaknesses from explicitly starting as a teaching language, the damage had already let C pull ahead.

As for nowadays, I'd say there are three things that help people to bounce:

1. The big name (Delphi) is proprietary and Free Pascal's documentation feels like it hasn't caught up with various lessons that were learned about how to document a toolchain and standard library since the early 2000s.

(And that's before you discover that, apparently, the API documentation is manual enough that the official stance on the Free Vision TUI component's documentation is "go find a copy of the Turbo Vision book from the Borland Pascal manuals", and that the Free Pascal Wiki either neglected to mention one or two classes when they were saying what is yet to be reimplemented or neglected to mention additional restrictions present in the DPMI port.)

2. Similar to with Ada, the ecosystem people are normalized into as they learn programming is leaning more and more strongly on the C-descended syntaxes, making Wirth-style syntaxes feel more alien.

I have to admit, compared to something like Rust, there's a certain off-puttingness to having so much verbosity and ceremony in the structure of the block constructs. To exaggerate a bit to get the point across, it's sort of like Pascal expects me to remember all the layouts and lines for what an empty tax form looks like well enough to draw it from memory before filling it out... and that sense of discomfort doesn't go away if I delegate it to my code snippets tool.

It lends an ambient sense that there's an iceberg of structure I don't understand and Pascal expects me to remember how many separate peaks it should have poking out of the water and where they should be, without me understanding the topography of the submerged portion.

(And yet I still recommend it as the Java/C# equivalent for DOS retro-hobby computing, since it's safer than C and has a much richer library of bundled functionality and comparable performance.)

3. The Free Pascal APIs are aggressively 90s and don't have enough examples to break you from your 2010s-and-beyond expectations. (I was almost tearing my hair out over how to get status updates from their zip extraction code before I realized that I was fixated on the Qt/GTK/DOM/etc. idea of using some kind of signal.connect(my_callback) function rather than using subclassing to set an event handler.)

Just, in general, it's an experience that's alien to current language trends and growing more so, the documentation available before you're committed enough to pay for it is wanting, and if you're willing to pay hobbyist prices, you need to do your own research on what exists to pay for.

You should try Haskell. It has everything Rust has (and much more) and it also has a garbage collector. I prefer it to ocaml.
Have you heard of https://mun-lang.org/ ?

It's an embeddable scripting language with the goal of being a Rust-like language that supports hot reloading of functions AND data. To achieve the latter, it uses GC'ed memory such that memory can easily be mapped when the memory's type changes.

It's still in early development but maybe one day will serve your needs :)

Rust does not have to be this hard.

Most of the pain here come from the unholy trifactor: combining async, lifetimes and dynamic dispatch with trait object closures; which is indeed very awkward in practice.

Async support is incredibly half-baked. It was released as an MVP, but that MVP has not improved notably in almost three years.

There are lots of ideas, some progress on the fundamental language features required (GAT, existential types, ...) and the random RFC here and there, but progress is painfully slow.

This isn't because no one cares, but because Rusts async implementation (which is very cool due to the low overhead) and its interactions with the other language features require complicated extensions to the type system. It does seem to me like there might be a lack of resources/coordination/vision ever since the Mozilla layoffs, but that's a different topic.

If you can avoid async I would recommend doing so. The problem is that the entire ecosystem has completely shifted to async. There are almost no active / popular libraries related to network IO that haven't switched over.

To be clear: using async is fine if you know what you are doing, and it can provide incredible performance. But if you do, keep it simple: avoid lifetimes and most importantly: don't attempt advanced trait shenanigans - if you do need traits, just returned BoxFutures without lifetimes, throw in lots of clone(), share as little data as possible, use Arc<tokio::sync::Mutex<_>>, and call it a day.

> If you can avoid async I would recommend doing so. The problem is that the entire ecosystem has completely shifted to async. There are almost no active / popular libraries related to network IO that haven't switched over.

Yes. I've been complaining about async contamination for some time. I'm writing heavily threaded code, with threads running at different priorities, and libraries which want async get in the way.

If you look at the poster's example, the "Arc" version is very close to the Go version. And if it didn't use "async", it would be even closer.

Go's green threads simplify things. It's real concurrency; you can block. But there are times when you have to lock.

As I've said before, if you're writing webcrap, use Go. The libraries for web-related stuff are stable and well-exercised, since Google uses them internally.

I agree, I don't understand why so many people lately seem to want to use Rust for web domain stuff.

I don't like Go, I hated the year I had to work in it @ Google. But frankly, it's better suited for 'server' type stuff, unless you're talking about a very specific type of server that has super intense latency guarantees. And now that Go has generics, I'd probably hate it less.

Go is the new Java. Rust is the new C++. Let's just stick with that.

Why? Because it's stupid fast, fast means serving an order of magnitude or more clients before requiring scale up. Scale up means $. No stop the world GC time situations, etc. That's basically it.

To be fair, I usually prefer to use go as well, lately though rust is more appealing.

I mean, there's no world in which my personal tastes wouldn't prefer Rust over Go -- I quite dislike writing Go. But as an engineer it is my responsibility to use the right tool for the job, not pick tools based on my tastes or gut feelings.

There's different kinds of fast. For most kinds of fast that people doing 'web' type things need, a garbage collector and a VM are not going to be the bottleneck. Efficient management of workloads across blocking I/O is going to be where the hard work is.

Now, I've written ad servers and video streamers, and other low latency high throughput things, and yes, I'd probably reach for C++ or Rust there. But some of the jobs I've seen lately posting for Rust, I do question. Even if I'm tempted to apply, because I'd like to get $$ to work in Rust.

Yea it's hard to say without context, I just gave the generic answer. I'm not sure you ever 'need' a garbage collector, and if avoiding one lets me save 20k in cloud expenses a year on a small team, it's probably worth it, because after scale out 20k isn't 20k anymore.

That said, people misuse technology all the dang time(I've done it). And in general I agree, Go is usually enough and has the best cloud ecosystem. I've had trouble with large go code bases exploding over time and requiring a lot of bodies to maintain compared with rust/scala.

Sometimes it's hard to speculate when one tool is clearly better than another. Some companies just want to use new stuff to sound cooler...

Rust does not in general have an order of magnitude advantage over Go. 2 or 3 would be closer, and that's with some nontrivial attention paid to optimization, not "you write Rust and it's automatically always faster".

Super high-end stuff can outclass Go by that much, like if you're seriously using DPDK or something, and in those cases I strongly recommend Rust over Go. There some other noches like that. But in general, it's not a factor of magnitude.

It really depends. I agree it's not always an entire order of magnitude didn't mean to paint that picture for passerbys who don't know better. Not trying to touch the "this lang vs that lang performance" conversation with a ten foot pole...
10x improvement over Go is a reasonable expectation, in a limited set of environments, generally at a scale where you're looking at using every last bit of a 32-core or 64-core machine and doing a lot of memory work. You should also expect to be spending a lot of time optimizing that Rust to get there. But when you need that level of performance, you also basically made a mistake starting with Go in the first place.

However, by far the bigger problem is people thinking their little web service serving out 5 requests a second requires that level of optimization when in fact even a Go implementation will use <1% of the CPU and no other resources to speak of. Dynamic scripting languages, IMHO, have skewed a lot of people's performance views. Go is already more power than most problems need, in terms of raw performance, and it's only a sliver, a niche where that's the difference between a Go vs. Rust choice.

It's a delicate understanding, but an important one for a professional.

I would not suggest using DPDK with Rust, unless you want to write a lot of stuff for yourself and wrangle with some tricky `unsafe` code. Sticking to C or C++ will make your life a lot easier.
The problem is there is more than one kind of "fast". There is latency "fast" and throughput "fast".

For a lot of web stuff you need throughput more than you need latency "fast".

In order to do throughput well you need concurrency.

There’s also how much it takes to hire 5 developers ”fast”. At least for now, Rust is not that performant on that respect.
Pretty sure you could hire 5 rust developers pretty easily. A lot of people in the rust community are dying to find a job that will let them write rust professionally...
It's at most 50% faster than Java and C# and not faster than C++. An order of magnitude would mean 900%, not 50%.
Perhaps in TechEmpower benchmarks, which to be fair aren't the worst way of taking frameworks out for a run to see what they can do, but at the end of the day they're very synthetic workloads. Interesting applications are piles of business logic where a slightly faster framework doesn't really buy you much.

Anecdata are all I can provide, but the equivalent (Google) batch job in C++ vs. Java is often an order of magnitude better for a variety of reasons. It might only be 0.5-3x faster, but it also uses less cores and less memory to do the same amount of work.

Having written C++ and Java services, the median C++ service is performs better than the median Java service. Some of that is less pointer chasing, some of it is better libraries. Some services will be slow no matter what you write them in. There's too many variables to quantify, and "performs better" is a real load bearing term. Sometimes it's less CPU, memory. Sometimes it's latency.

I still primarily work on Java services, and they perform pretty well. That said, the operational dynamism of Java services is a real thorn in my side. Beyond the GC, there's a lot going on in the runtime including JIT that just makes JVM services less predictable. Even classloading is a source of unpredictability, unless you eagerly load classes.

FWIW, I think Go does a pretty good job being predictable too. AOT compilation helps there, you're not worrying about new tasks needing to JIT. Go mostly worries about GC (often irrelevant), warmup of state (like TCP connections), and avoiding footguns that leak Goroutines. :) At least Go's footguns rarely blow your whole leg off.

In practice, on bigger applications Go very much fails to be faster than Java or C#.
The point was about Rust. Go is on par with C# and Java.
Sure, just many people do believe that Go is somehow faster than the other two, so I thought it would be worth to chime in.
>"I don't understand why so many people lately seem to want to use Rust for web domain stuff."

>"Rust is the new C++. Let's just stick with that."

I write "web domain stuff" in C++ and it is incredibly easy (well for me at least). In C++ I could always use my own styles / paradigms / patterns etc. etc. Not forced to any particular way. And modern C++ is incredibly safe if one wishes.

So if it is bad idea to write "web stuff" in Rust it means it is anything but new C++

I wrote plenty of vaguely web stuff in C++ at Google. A lot of services at Google are C++. But it's kind of, not how the rest of the industry expects things. And most of that stuff there is now moving to Go.
>"not how the rest of the industry expects things"

I run my own company, prefer to count money and Have no shortage of work - so could not care less about what "the rest of the industry thinks".

>And most of that stuff there is now moving to Go.

Go is on the same page performance wise with Java and C#. Sure, if you don't need the best performance, you can move to another language. But, then, why did you start using C++ in the first place?

I doubt pretty much that SAP and Adobe would rewrite their enterprise products in Go.
> In C++ I could always use my own styles / paradigms / patterns etc. etc.

That's also one of its major disadvantages, unless you literally rewrite the entire thing when major maintainer changes happen, because otherwise you get a mix of different C++ styles in your codebase which leads to nobody being able to maintain it.

I don't know, I never got heavily on the C++ inheritance bandwagon when it was popular in the late 1990's early 2000's. Rather preferring aggregation.

If you application is actually using the OO parts of the language that way, even without modern C++ its fairly easy to maintain as the different styles/etc area also encapsulated in their classes. Then hopefully the top level is using some kind of message passing interface/whatever to avoid trying to glue everything together into a god class.

AKA, there are a few fairly easy to understand rules that allow people to do their own thing without creating a maintainability nightmare even with very large C++ codebases. If an experienced engineer/architect/etc with a track record of successful C++ projects is in charge during the initial application design/etc you should have a fairly maintainable application.

I'm not sure this is really a C++ thing though, rather being a general engineering thing. If the main architecture is well thought out and understandable, a lot of sins can be burred in places where they can't create application wide chaos.

There are a lot of things that can make C++ applications suck, but you don't tend to hear about the success stories on your favorite board, those systems silently do their job. So many of the things people rail about with C and C++ simply aren't problems when appropriate engineer culture is maintained. AKA, having solid unit tests for most of the base classes being agragated, means that running them under various address sanitizer/etc tools will find the errors that aren't picked up by static analysis tools/etc.

Yes, sometimes things sneak by, but i'm not sure there are any languages java/rust/etc that solve that problem completely.

> because otherwise you get a mix of different C++ styles in your codebase which leads to nobody being able to maintain it.

I'd be really interested in meeting someone who has enough mental flexibility to learn C++ but not enough to accommodate different code styles in a single codebase.

Yes. It is always easy to see what generation a piece of code is from. Trying to maintain a "consistent style" just amounts to failing to improve.

If you are not embarrassed by your old code, you have not learned anything since.

I rely on 4 libraries in total when doing web apps in C++ (postgres, http, json, logging). Not a single one ever gave me any surprise of the kind.

I use other libs of course but those are domain specific and are solving non web problems

Modern C++ isn't really safe. Safe means the compiler catches you, generally C++ compilers don't. Trivial example is iterator invalidation -- even with all warnings and errors on compilers don't catch it.
> Modern C++ isn't really safe.

Can we cut it out with the hyperbole? Using "It Isn't Really Safe Unless It Is Written In My Favourite Niche Language" as an argument is .. well ... ridiculous. You can extend that argument to any practical programming language.

So ... Rust ... "Isn't Really Safe" because you can get into a point where memory is corrupted, where your application deadlocks, or threads starve, etc.

Haskell ... "Isn't Really Safe" because it is not possible to formally verify the logic.

Etc, ad infinitum ...

Maybe go for "Not As Safe As". After all, I don't even like C++ (see my comment history), but it's certainly possible (and not very hard) to get about 90% of Rust safety in C++.

Safety is not a binary, it's on a spectrum. Saying that a language is either safe or unsafe implies that the "safe" language is actually safe while the "unsafe" language is completely deadly. That's certainly not true.

> Rust ... "Isn't Really Safe" because you can get into a point where memory is corrupted

i dare you to find memory corruption in safe rust that isn't already on an issue tracker.

> Saying that a language is either safe or unsafe implies that the "safe" language is actually safe while the "unsafe" language is completely deadly.

tell that to the people who got owned by https://googleprojectzero.blogspot.com/2021/12/a-deep-dive-i... , most likely human rights activists targeted by less-than-savory regimes. memory corruption bugs are so frequent that it is possible for nso group to sell to pretty much any regime, and not forced to be classified as top secret information.

> i dare you to find memory corruption in safe rust that isn't already on an issue tracker.

can't you just mess around with /proc/self/mem :)

That one was already reported to the tracker :)

https://github.com/rust-lang/rust/issues/32670

(To be clear, I’m not saying that there’s no issues that aren’t in the tracker yet. But there are a bunch of them that are. More will absolutely be found as time goes on, that’s just how these things are.)

Thanks, that was an amusing read!
Not a Visual C++ user I imagine, because that is exactly what enabling _ITERATOR_DEBUG_LEVEL does.
So we should all be using Spark Ada?

Safety isn't an all or nothing thing, and in most cases, rigorous safety comes with tradeoffs. In some applications those tradeoffs are worth it, and in others they're not. A small project probably doesn't need to be provably correct, because it's easy enough to analyze it, and you aren't gaining much if the more rigorous language is unwieldy (they often are); similarly, a large enterprise project is harder to analyze, and has larger costs if there are CVEs, so a safer language is probably a good fit. Note that this doesn't necessarily mean rust, though -- garbage collected languages sit here, too. Rust sits in the niche of "big enough and / or critical enough to justify rigorous safety", and "high performance really matters".

That's not my experience. I'm 3 times more productive at writing web apps in C# than in C++ and that is not even taking into account compile times and hot reload and doing unit tests.

Maybe I'm the worst C++ programmer in history and maybe I didn't spent too much time in trying to write web apps in C++ - it was just to test if it's a viable approach - but that was my particular experience.

Aside from doing more boiler plate code, the language being more ceremonious, I find that I miss the tools like frameworks and libraries which are very easy to integrate with each other and which I take it for granted in C#. Probably the story is the same with any other language used for web: Java, JS, Ruby, Python and even the (in)famous PHP.

I think the situation can be much better if someone would make some nicely designed frameworks and libraries, but I guess no one is interested to as C++ is perceived as a "not for web" language. People who are into C++ are generally systems programmers who are not into Web, and people who are into Web are taught they only have to use "web languages".

That is really a shame because for some situations there would be a huge benefit of having performant web apps - scaling out is not always a solution to a performance problem.

Depends on the ecosystem, while I agree with you, at least on Microsoft stack there were always nice tooling to write C++ applications for web apps.

Before .NET we had ATLServer, then C++/CLI.

They also published some frameworks for writing web APIs in C++.

Now, I would also advise C# and then if needed, to call into C++ via P/Invoke (or C++/CLI, C++/WinRT if on Windows), than exposing C++ directly into the wire.

>"I didn't spent too much time in trying to write web apps in C++ - it was just to test if it's a viable approach - but that was my particular experience."

I rewrote web apps written in PHP and Python. Those were rather decent size and in my case app specific code was about the same size in C++ as in the other 2. The performance was of orders of magnitude better.

My applications do not contain millions line of code and maybe because of this and the way the code is organized I do not really suffer long compiling times. Usually it is just few seconds. Good enough for me.

I really don't believe that this high-level low-level language thing is true. Sure, both C++ and Rust are incredibly expressive and cool languages but they want to give control for low-level details, yet make it very easy to ignore these for the most part. But there will sure come a time when you will have to (may not happen at first write, but will definitely happen at refactor, adding a new feature, etc). Managed languages make such refactors trivial, while C++ and Rust (even with its very advanced type system) make these harder due to you having to rearchitect the whole program from a memory model perspective. Sure, it can be trivial in many cases, but not always.

So all in all, I really don't think that the (long-running) productivity and maintainability of managed languages can be approached by these low-level langs. And that is fine, (thank God) not everything is a dumb CRUD web app, there are very real niches where that low-level detail is a necessity.

>"Managed languages make such refactors trivial, while C++ and Rust (even with its very advanced type system) make these harder due to you having to rearchitect the whole program from a memory model perspective. Sure, it can be trivial in many cases, but not always."

This is not the function of the language but the ability of the developer to properly architect their code.

And tools like Visual C++ / CLion have very advanced refactoring features.

I do it first and foremost because I dislike the error checking story in Go, and writing Rust for web stuff doesn't really feel too dissimilar to hacking on web frameworks of years past. In fact, I'd argue that web domain stuff is where Rust is relatively mature.

But this all said, there's definitely a point to be made here - Go works fine for so many use-cases, people should use it if they like it or it fits the story better. The idea of "one true language" has never worked out.

I do it because the toolchain is so nice. I just do

  wasm-pack build --target web
And it builds a little wasm file and some js shim, and that's all it takes to get a Rust website up and running.
I find that part a hurdle, actually, despite loving Rust. I'm struggling with magic tools like wasm-pack that take over the build process. Cargo is fine – I know what Cargo does and how and why it calls rustc. But finding out what wasm-pack and friends do has been an uphill battle for me.

Why can't I just compile to wasm32 with Cargo? What's missing?

As far as I know you can using the wasm32-unknown-unknown target. I think wasm-pack does extra stuff like supporting different targets like a nodejs module or a webpack compatible format, which is outside of the scope of something like cargo imo.
But what is that "extra stuff" that it does?
>I agree, I don't understand why so many people lately seem to want to use Rust for web domain stuff.

Mostly because of performance. Looking at latest Techempower benchmark, stuff written in Rust is 50% faster than stuff written in C# or Java. [0]

If that difference in performance and the tradeoffs to obtain it are worth, that's for everybody to decide.

[0] https://www.techempower.com/benchmarks/

We use rust for web dev purely for the type safety. Having business logic encoded as a state machine in an enum with compile time checked matches makes the world of difference
Scala, F#, OCaml, Kotlin,...
I have no experience with these languages. But it's worth noting that Rust is somehow more popular than all of these for a reason.

Possibly a combination of type safety + speed in one bucket makes the difference

> Go is the new Java.

I hope not, it might have some generics now, but still has a lot to catch up with.

I surely don't miss coding in Java 5 (2004).

> Rust is the new C++

Rust might become the new C++, and while being safe by default is great, there are plenty of C++ use cases where Rust has zero presence in 2022.

Generics were around for some time now, and people don't seem too keen on using them for anything aside from data structure specialisation, because frankly dynamic dispatch via interfaces hits such a sweet spot where you get a lot done with a very minimal overhead. The added syntactic complexity is very rarely justified, and containers is perhaps one of those cases where it is the case. Otherwise, from what I observed— generics in Go were massively over-hyped, but failed to gain the traction that was initially expected from it. I feel like there's a very good reason for that.
Generics as a feature is almost invisible when it exists and is done well, but is a huge pain in the ass if it doesn't. It is mostly needed for libraries, so your average dev won't write them often, but there it is invaluable.
Yet even Objective-C, eventually got generics in 2015, despite protocols and categories.

The good reason is the target demographics as Rob Pikes puts it, people that don't care about learning what programming language can be like.

I love borrow-checker and lifetimes (and RAII). It is pretty simple to implement a REST API in Rust.
>As I've said before, if you're writing webcrap, use Go. The libraries for web-related stuff are stable and well-exercised, since Google uses them internally.

I don't get why are you so dismissive about web programming? The Go libraries are good because it's easy to write good libraries in Go. And it's easy to write good libraries in Go, because of design decisions. Same for C#, F#, Java, Python and more.

Go's ecosystem is amazingly stable. Its rich standard library helps, but in general, there's a lot of attention to backward compatibility.

Once an application has been written in Go, updating dependencies or using a more recent compiler version is a breeze. Nothing breaks.

Rust code on the other hand comes with a high maintenance cost. The ecosystem is still very unstable. Core dependencies constantly have breaking API changes, or get abandoned/deprecated/superseded. Keeping everything up to date is not trivial and very time consuming. I abandoned projects due to this, and others use dependencies with known vulnerabilities, that may not even compile any more at some point. But dealing with changes in Rust dependencies instead of the actual application logic is not fun.

So, for a project that has to be maintained long-term, I would choose Go anytime. Productivity is so much higher, especially when including maintenance cost.

> The ecosystem is still very unstable. Core dependencies constantly have breaking API changes, or get abandoned/deprecated/superseded. Keeping everything up to date is not trivial and very time consuming.

I'm only a hobby coder but this has hit me a few times. I suspect this will level out in time, though.

This has been discussed on the Rust forums. There are too many widely used packages that are stuck at version 0.x, with no stability guarantee. The basic HTTP library, "hyper", is at 0.14.19, and it's had breaking changes more than once. 48,300,708 downloads.

* "image": 0.24.2 (Read and write common image formats, 7,257,076 downloads)

* "glam": 0.20.5 (Basic 2,3,4D vector and matrix operations, 1,155,849 downloads)

* "cached": 0.34.0 (Cache management, 1,367,883 downloads).

Rust needs a push to get everything with more than a million downloads up to version 1.x. Then the semantic versioning rules are supposed to require no breaking changes for existing code without changing the major version number.

> The problem is that the entire ecosystem has completely shifted to async. There are almost no active / popular libraries related to network IO

The ecosystem, or the ecosystem where network IO is a thing? Surely that's just a corner of the Rust library ecosystem. I have almost never used network IO (databases, http,...) in 20 years of programming, and zero times in Rust.

There is a big (and might I say extremely comfortable) world of programming everything is on one machine, and things are CPU instead of IO bound.

There are many use cases of raw socket programming in the C++ niche. Anything that requires high and predictable network performance .
> To be clear: using async is fine if you know what you are doing, and it can provide incredible performance. But if you do, keep it simple: avoid lifetimes and most importantly: don't attempt advanced trait shenanigans - if you do need traits, just returned BoxFutures without lifetimes, throw in lots of Arc<Mutex<_>>, clone() and call it a day.

It seems like everyone doing async Rust goes through a long journey before arriving at this conclusion. Once you know the pitfalls you can navigate around them, but I hit a lot of dead ends along the way.

Anecdotally, this is the pain felt most across the entire rust language. There are more ways not to do something than to do something. This makes it hard for beginners to pickup, and difficult for projects to scale on.
I've heard people complain about languages having too many ways to do one thing. Never have I heard the opposite complaint hahaha. You can't please everyone.
That by itself can be interpreted as a good thing -- no Perl TMTOWTDI please.

The "special quality" about Rust that hurts here is that it takes a lot of time going into a dead end before you realize it's a dead end.

Completely agree. I wrote a multithreaded compute-heavy program in Rust, semi-ported from a C++ version, and sidestepped async completely—just used old school thread primitives. It was delightful! Once I sorted out the data structures and messaging, the borrow checker and Send/Sync traits made implementation nearly trivial, and absolutely no memory corruption or accidental non-atomic clobbering. It took a bunch of hours with gdb and valgrind to achieve the same stability with the C++ code, and to this day I'm not 100% sure I got every edge case.
I’m a Rust newbie. Mind if I ask: are you referring to using threads and locks and queues and such?

Does Rust give you rope to hang yourself when doing it without async or does it continue to be very specific about forcing you to guarantee that you’re not going to run into races and whatnot?

Oh my god we’ve strayed so far from the light T_T

Once upon a time, the whole promise of Rust was “fearless concurrency”.

I don't understand. What about this thread would make you fear running rust? Fear of learning a solution to rusts memory model, and 'fighting with the borrow checker' as they say -- sure. But rust never claimed that you wouldn't have to climb a steep learning curve, it claims that if you do make it up then the result can't hurt you due to a memory fault. So "fallen from the light" seems a little overdramatic.
IMO fallen from the light sounds spot on (not gp). I’m shocked to hear the state of async from real practitioners saying to avoid it.
(comment deleted)
"fallen from light" would mean breaking it's safety promises. No promises have been broken, the async developer experience is just less than a silver bullet and needs a lot of work. If you're so disappointed by that fact that you'd say rust has "fallen from light" then you've not been paying attention at all or your expectations need serious calibration.
Fearless concurrency in respect to threading .... Not async
Rust marks cross-thread shared memory as immutable in the general case, and allows you to define your own shared mutability constructs out of primitives like mutexes, atomics, and UnsafeCell. As a result you don't get rope to hang yourself with by default, but atomic orderings are more than enough rope to devise incorrect synchronizations (especially with more than 2 threads or memory locations). To quote an earlier post of mine:

In terms of shared-memory threading concurrency, Send and Sync, and the distinction between &T and &Mutex<T> and &mut T, were a revelation when I first learned them. It was a principled approach to shared-memory threading, with Send/Sync banning nearly all of the confusing and buggy entangled-state codebases I've seen and continue to see in C++ (much to my frustration and exasperation), and &Mutex<T> providing a cleaner alternative design (there's an excellent article on its design at http://cliffle.com/blog/rust-mutexes/).

My favorite simple concurrent data structure is https://docs.rs/triple_buffer/latest/triple_buffer/struct.Tr.... It beautifully demonstrates how you can achieve principled shared mutability, by defining two "handle" types (living on different threads), each carrying thread-local state (not TLS) and a pointer to shared memory, and only allowing each handle to access shared memory in a particular way. This statically prevents one thread from calling a method intended to run on another thread, or accessing fields local to another thread (since the methods and fields now live on the other handle). It also demonstrates the complexity of reasoning about lock-free algorithms (https://github.com/HadrienG2/triple-buffer/issues/14).

I find that writing C++ code the Rust way eliminates data races practically as effectively as writing Rust code upfront, but C++ makes the Rust way of thread-safe code extra work (no Mutex<T> unless you make one yourself, and you have to simulate &(T: Sync) yourself using T const* coupled with mutable atomic/mutex fields), whereas the happy path of threaded C++ (raw non-Arc pointers to shared mutable memory) leads to pervasive data races caused by missing or incorrect mutex locking or atomic synchronization.

Re: fearless concurrency... Would Rust prevent you in general from writing code that could deadlock, btw?

Thread1: takes lock A, ..., tries to take lock B

Thread2: takes lock B, ..., tries to take lock A

Looks like you should be able to pass Mutex<A> and Mutex<B> to both threads otherwise what's the point of mutex if there's no way to share data protected by it, so it doesn't look like it prevents you from hitting this scenario.

Rust doesn't protect you against deadlock.

I wonder if it is similar to the halting problem. Can deadlocks even be prevented in theory?

Only if the priority is flat and not some sort of graph, is my guess.
In the general case you're right, it's equivalent to the halting problem. The outline of the proof by reduction: set up two communicating processes in a way that will deadlock iff a particular loop in one process fails to terminate. So if you had a deadlock detector for arbitrary communicating processes, you could use turn it into a termination detector for arbitrary loops.
Yes, deadlock-free lock systems are an ordinary part of OLTP database engines. They don't prevent deadlocks per se so much as detect them and dynamically resolve them.

The mechanism is costly but elegant. If a lock you are trying to acquire is owned by another thread, you inspect the locks you own to determine if that thread is waiting on one of your locks. When a deadlock is detected, there are several strategies to automatically resolve it e.g. rolling back one of the threads to a point where forward progress can be safely serialized.

No one wants to use these mechanics for ordinary code, due to their cost. For the fashionable thread-per-core software architectures, deadlocks aren't something you commonly have to worry about.

No, Rust doesn't prevent deadlocks, a deadlock is safe (it isn't what you wanted, but it's safe). There are well-known strategies to avoid deadlock (in any language)

In the trivial example you gave, one strategy just insists we take locks in alphabetical order. Thread 2 can't take lock A, because it already has lock B and that's not the correct order.

I'm pretty sure "fearless concurrency" is just a meme at this point since Rust does very little to make concurrency "fearless".
Very little except statically removing an entire class of bugs that plague other languages in the same design space you mean.
There are two kinds of threading bugs: deadlocks which are easy to detect and race conditions which are far more difficult to detect and to fix. AFAIK Rust helps with the latter not with the former which is a very big improvement (much more than if it was the other way round)
Just to add, AFAIK Rust only prevents data races, not race conditions in general. Which is still a huge help, but concurrency is still hard without a much more restricted model.
No, deadlock-free code requires some additional structure. There is no general way that I know of to prevent deadlocks in any software with non-trivial lock graphs, but there are standard techniques to detect deadlocks programmatically so that they can be broken and resolved. OLTP databases figured out how to do this decades ago, but those techniques are expensive for general purpose programming.

The common method for deadlock-free code is roughly that when a thread is required to wait on a lock owned by a second thread, it checks to see if the second thread is waiting on a lock already owned by the first thread. This requires that the lock graph essentially be a high-performance and concurrent global structure.

Locks like this can be expensive, particularly under high concurrency or contention, so they aren't used for most software. If you can fit your software in a simpler model e.g. where locks are singular or only acquired as a DAG, then much higher performance options are available that don't require deadlock detection.

I really appreciate this detailed response. You gave me plenty to base my searches and reading on.
Yeah, but it doesn't help on shared-memory process concurrency, and we all know that in 2022 the best way to ensure secure software is to go back to processes.
Yes, Arc for structured data passed between threads, atomics for smaller things like counters job queue length. Of course the fastest synchronization primitive is nothing at all :)
It has been a sec but if I were to do another multi-threaded async Rust project I would do one thread per async runtime and explicitly pass anything that needed to be shared.

This should be more ergonomic as this should get rid of everything needing to have send/sync traits. I also suspect it may be more performant as I am not sure how good the async runtimes are about keeping scopes pinned to a particular core so its not constantly jumping around and busting the l1 caches (which would be extremely detrimental to compute latency and bandwidth)... Happy to be schooled on any of this.

But what when you have some threads slacking off, and others too busy? It would be nice in this case to use those idle threads, even if it means a little bit of CPU cache trashing. And I believe this is what Tokio offers with a work stealing thread pool.
True, but I suspect that without a truly global prescient scheduler it is almost never worth it to core switch unless you generally have really long tasks.

For an efficient core context switch the scheduler must accurately predict that the source (current) core won't be free for the duration of the full core context switch time and that the sink core will be free by the time the meta context gets there and will have been free by the time the rest gets there. Otherwise, the scheduler ends up thrashing the cpu (it is actually a bit worse as future task might need same context so you have to be aware of the future). So, for the scheduler to know this it would need to be:

- Global: The only scheduler on the system or basically rafting with all the other schedulers on the system

- Prescient: The scheduler(s) would need to be able to predict all tasks, thier context, and work time per task perfectly. Which could really could only happen when everything is static and hence deterministic.

For example, I think most tasks people are throwing at async are web requests. Most actually take the core an order of magnitude shorter time to compute then the time it takes passing the context from one core to another and they are all unpredictable to the scheduler. In this scenario I could see the scheduler taking up the majority of computational time on the system. So turn on multi-threading + async on a quad core and you will get worse bandwidth and latency(always) for all your pains.

EDIT: Although this single data point would tell me I am wrong (see description):

https://www.youtube.com/watch?v=IG-wGXENTt8

Look at Glommio, it’s essentially what you describe.
Thanks for the link, very helpful :)
> This isn't because no one cares, but because Rusts async implementation (which is very cool due to it's low overhead) and it's interactions with the other language features require complicated extensions to the type system. It does seem to me like there might be a lack of resources/coordination/vision ever since the Mozilla layoffs, but that's a different topic.

I wonder how much technical debt is slowing things down. It seems like we keep hitting a breaking point where we'll finally be forced onto polonius and chalk but people keep finding ways to extend the existing implementation to make things work, kicking the can down the road.

I might be (probably am) missing something, but for the use case described, it seemed odd to me to have borrowing come into play at all. I would think that rather than a borrowed reference with a lifetime, the dispatcher could just consume the object coming in. The term “moving” is a bit misleading in that it carries with it an implied heavyweight operation that likely doesn't come into play at all.
In your experience what languages would you say handle async well? Genuinely curious. I’ve only ever done JS professionally for a decade but started branching out into python, rust, and kotlin due to personal projects.
Interestingly, I've become a big fan of node's single-threaded "one big loop" model, which means multitasking is cooperative instead of preemptive. This strikes me as more honest, somehow. It doesn't distract you with abstractions (like threads) that don't make sense in this context. Most production workloads these days will be a docker process assigned to (at best) a single sticky core/thread on a blade somewhere - so in terms of resources, node is quite honest about what you actually have to work with. (This as opposed to, say, a Java process, which wants to believe it has control over an entire physical server CPU, and when you run it in a docker process, you're just exercising virtualization overhead if you use Thread, etc.)

That said, if you have a physical server, Java is quite good, especially with the upcoming Project Loom improvements. The langspec and vmspec have gotten fat the last 20 years, but at least those documents exist. Plus there is OpenJDK which is a great enabler and calmer of nerves; there's a reason so many alt langs target the JVM, and they are good. Groovy, Clojure, Kotlin, Scala are all first-rate languages, IMHO. And with projects like Quarkus you can more easily build native executables that bundle the JRE and make distribution very Rust-like.

Another environment I like specifically for async operation, but mostly by reputation, is Erlang and it's BEAM VM. Erlang itself is such an interesting language, being dynamic, functional, immutable, without traditional control structures (!) but relying heavily on recursion and pattern matching, and of course the "actor model" and extremely lightweight "processes" was invented here (and promptly ported to elsewhere, as with Akka). It was also created by one of the nicest human beings I've ever experienced, Joe Armstrong, may he rest in peace.

IANALL so YMMV.

> Plus there is OpenJDK

I'm not sure what you mean here, but if you refer to OracleJDK here then there is basically only OpenJDK for quite some time now -- OracleJDK is just an (optionally) paid support version of the same codebase. Also, most other vendors are pretty much just tiny patched OpenJDKs also, with some niche exceptions.

You must be young. Java was not always distributed like this, and although it was "open source" few people compiled it, and the JRE and JDK was distributed by Sun (then Oracle) primarily through a user interactive web UI. The OpenJDK existed alongside this for some time, but then supplanted the proprietary binaries. That was a relief because it meant Java was actually (not just theoretically) open source now, which meant it was safe from deprecation, disablement, and all the other negative aspects of control that come with de facto proprietary software.
Server workloads are different from client workloads here, and network/IO heavy workloads are different from CPU heavy workloads. You'll get tend to get misleading advice from people only familiar with one of them. Especially if the client is an asymmetric architecture like ARM has and Intel is moving to.

Swift's design is made to be good for IO workloads on smaller clients, though it hasn't got as many tools for the other end.

Haskell has always been the best. Go's did the same as Haskell. In either case IO is automatically async. If you want concurrency you create very light weight threads. This approach works very well for most use cases, but when you want the best performance possible the light weight threads can still be too much. Zig is going for a lowest possible overhead approach like Rust but has an interesting take: https://kristoff.it/blog/zig-colorblind-async-await/
Are you sure you are talking about Haskell and not Erlang? I'm not aware of how Haskell IO would be "automatically async".
That's definitely a description of GHC Haskell. All network IO in Haskell goes through a subsystem called the IO manager, which makes use of platform appropriate high-performance non-blocking APIs. (Actually, I think windows isn't getting an IOCP implementation until the next major release, but Windows has always been a bit undersupported by GHC.)

The nice thing about this is... You don't have to care. Haskell is a good enough programming language to just put the platform-specific non-blocking hell APIs in a library and let you write code that looks linear and blocking. If you want more control you can get down to the low level non-blocking APIs, but that's usually not going to be worth the trouble.

Yeah, many people don't even see a reference about Haskell IO being async. That's what the "automatic" part is doing.

Haskell IO is basically as async as Javascript, as in every operation is fully asynchronous, you need to call some foreign function if you want otherwise. Except that you have parallelism and can have concurrency too added if you want.

Any language which is dataflow based will be great in an async context. Async is hard in most languages because they are imperative, which stands in stark contrast to the whole idea of asynchrony. This whole exercise of adding async features to various imperative languages is coming at the problem from the wrong way around in my opinion. It's a recognition that async programming is hard in imperative languages, and the thought is that maybe this could be made better with more language features. But the sad truth is that async features clash hard with the imperative nature of most mainstream languages.
I cannot, for the life of me, recommend Elixir enough! You write your code without every thinking of words like "async" or "await" and the VM handles it for you!
So this. You have to learn to think differently about your problem. But then when you do, so many of these other issues just go away. A small amount of our product offering is implemented in Elixir. I wish more of it was. It’s my favorite part of the whole thing.
Credit where it's due, this is all thanks to the BEAM and OTP being awesome.
Go is the prime example.

For example, in your typical web application a Goroutine is spawned for every incoming connection. This basically gives you a dedicated runtime for each simultaneous connection. In Node or Python, this would be the equivalent of starting a whole new process for every request. But in Go, there's very little cost to doing it this way.

The advantage is that each connection basically never blocks waiting for another user.

In Node and Python, we talk about handling tens to hundreds of requests per second per server.

In Go, we talk about handling thousands to tens of thousands per second. It's just orders of magnitude more throughput.

Can't comment on Python, but I once worked on a Node.js codebase that could only handle tens of requests per second, it was highly unusual and doing some incredibly dumb things like pinging out to Redis to check for DDOS protection counters before starting to serve any request.

See: https://www.fastify.io/benchmarks/

Node is more susceptible to poor code and out of band IO slowing it down, but the V8 runtime itself is cpp and simple services are close to just writing decorators over a cpp webserver. It's obscenely fast and good enough for most purposes.

> Most of the pain here come from the unholy trifactor: combining async, lifetimes and dynamic dispatch with trait object closures; which is indeed very awkward in practice.

Even in regular Rust, trying to get too clever with lifetimes can cause serious pain. The usual culprit is complex code that tries to never allocate memory. "Oh, well this closure borrows this parameter from the parent function, and then stores a reference to it in this stack-based structure inside the closure, and then we pass everything by reference to this higher order function..." Just say no.

If you add async to the mix, then you need to keep your ownership simple. If you have a long-running async function, then pass parameters by value! If you have a polymorphic async function, then return your result in a Box. (This also breaks up the generated state machine used to implement async functions, and can reduce binary size.)

So much of this pain is caused by premature optimization.

In C++, if you get to clever, you eventually make a mistake and segfault. In Rust, if you get too clever, it eventually becomes impossible to satisfy the borrow checker. Most of the time, the solution is to be less clever.

Async Rust was a fascinating experiment, but in practice, I think it turned out to be a tool that's best used conservatively. I've written some very stable and high-performance production code using async Rust. But I keep it simple when I can.

This is good advice. Write simple code first. Another generic thing I've come across is, "if the borrow checker is screaming at me when I hit a patch of code, I probably have a design flaw not a programming problem".

I've seen a lot of new people also prematurely over generalizing code. I know it sounds terrible to say, but if you probably aren't going to reuse it, you really don't have to prepare your code base just in case you might later. Decide that later and move along...

>"if the borrow checker is screaming at me when I hit a patch of code, I probably have a design flaw not a programming problem".

This reminds me: "you are holding it wrong"

In industrial machines, there's such a thing as holding it wrong. In some cases, that means getting maimed (that old-school table saw doesn't care whether it chews on wood or flesh) or precluded through a clunkier mode of operation (you need to press these two buttons separated at roughly arm length to make sure that they can't be actioned while you have an arm in the way of the heavy arm-eating chunk of metal). The former is C and unsafe Rust, the later is safe Rust and GC languages.

You're trying to draw a comparison with a consumer product with a design flaw.

>"You're trying to draw a comparison with a consumer product with a design flaw."

Not at all. I just do not like to dance around bonfire with tam-tam and being told that this is the one and only way

It's not the only way, but if you do the tradeoff of going with manual memory management, then you should accept what the tradeoff entails. Essential complexity is non-reducible -- you either manage it automatically through a GC or choose a model that while let you ignore it quite often, will make you think about it in the end.
> If you have a long-running async function, then pass parameters by value! If you have a polymorphic async function, then return your result in a Box.

I've taken to making heavy use of the smallvec and smartstring crates for this. Most lists and strings are small in practice. Using smallvec / smartstring lets you keep most clone() calls allocation-free. This in turn lets you use owned objects, which are easier to reason about - for you and the borrow checker. And you keep a lot of the performance of just passing around references.

I tried to use async rust a couple of years ago, and fell on my face in the process. Most of my rust at the moment is designed to compile to wasm - and then I'm leaning on nodejs for networking and IO. Writing async networked code is oh so much easier to reason about in javascript. When GAT, TAIT and some other language features to fix async land I'll muster up the courage to make another attempt. But rust's progress at fixing these problems feels painfully slow.

https://crates.io/crates/smallvec / https://crates.io/crates/smartstring

However the constant checking if something is on the stack or heap is a big performance problem for smallvec. It results in terrible cpu branch prediction.
I don't see how is it bad for branch prediction. Like, if you use the same smallvec object in a loop and do not insert any elements to it (so that it doesn't allocate on the heap) the branch will always turn in the same direction making it easy and cheap to predict. And I would think that in most use-cases your smallvec object will either remain in one state, not changing in-between.
+1, I would love to see a trace of smallvec killing performance due to branch prediction.
> If you have a polymorphic async function, then return your result in a Box.

Agreed. Copy, Clone, Box, Arc, RwLock, etc. are your friends. Don't be afraid to use them.

You don't need as much performance as you think you do. Passing things around on the heap is fine for most applications. The Rust compiler is often smarter than you think.

And, when the Rust compiler is dumb or your code is getting stuck, you have code that you can understand and find the hotspot of. Now you can be clever.

> So much of this pain is caused by premature optimization.

Part of the problem is that Rust itself has such lofty aspirations to do it all (particularly "abstraction without overhead"), and is largely successful at that. So if I compromise on efficiency, it feels like my code is unworthy of the language it's written in.

Also, it's easy to feel that the slightest inefficiency puts us on a slippery slope to the bloat and slowness of something like Electron. I'd like to fight back against that bloat, the upgrade treadmill, and the rapid obsolescence that serves hardware makers but hurts poor people. On the one hand, there was good software in the 80s and 90s that made liberal use of heap-allocated, reference-counted objects with dynamic dispatch. On the other hand, there were slow, bloated desktop applications before the rise of Java, C#, Electron, etc. So I don't know where the right balance is at.

You hit the nail on the head, at least for me. I've found Go to be the right sweet spot between efficiency and productivity. I can't get anything done in Rust. My worst enemy is myself.
I like the thick stdlib in go, but it feels a lot slower even than java. The thin stdlib and giant dependency trees is my primary complaint with rust. I like most of the syntax, but async is not my threading style, I mostly use fine grained threads like rayon provides. I'm currently more excited about julia though. Same dependency hell, but super fast and flexible, and the code looks good.
You think Java and Go are slow but like Julia? It takes 2 seconds just to fire up a Julia interpreter. Its runtime is extremely heavy.
well… you start it once and just use forever after that
That is what I usually do. And you can supposedly precompile everything if you just want to deploy a service so restarts are short. I have not done that though.
I agree; if you use dynamic `Arc`s almost everywhere in your code, what's the point in a systems PL whose essence is in managing static lifetimes? Sometimes people use Rust just because many other languages suck; they are choosing between two evils: tedious programming in Rust or inadequacies of another language. It should not be like this. This is why I think we need a high-level, no-BS version of Rust.
Which parts of Rust would you give up to make this happen? And how would this be implemented?
Hide memory management under a language runtime. Make a single Fn trait/string type instead of multiple ones. Add effect polymorphism to deal with function colours. Remove async/.await and express it using algebraic effects, do the same for streams and iterators.

How it'd be designed and implemented is probably a theme for a separate blog post, not a HN comment.

So basically OCaml.
OCaml is great, but it lacks traits / type classes. That plus some syntactic tweaks would make OCaml amazing for most general purpose programming.
That is what functors and modules are for.

And then the O in OCaml is all about OOP.

Functors and modules are nice but do not have the convenience of traits and type classes. The problem with functors and modules is that you have to manually instantiate the right functors and modules when you want to use some operation. Type classes and traits build the right instantiations for you based on the types.

I want to write `toString (1,true)` not `ToStringPair(ToStringInt)(ToStringBool).toString (1,true)`.

> Which parts of Rust would you give up to make this happen? And how would this be implemented?

Traits, maybe? Switch to an OO model more closely aligned to what the majority of developers understand. Dump all of the line-noise-type syntax.

My Own Toy Language (ComingRealSoonNow)^tm that I started designing had exactly one goal - prevent the majority of memory errors, not prevent ALL memory errors. All I wanted was to indicate when/where an object will be destroyed/mutated. That's enough for me.

Actually something like traits (as concept) is what powers COM, WinRT, the basis of Objective-C protocols that influenced interfaces in more common OOP languages.

Also the basis for one programming language that used to be widespread in the enterprise for quick and dirty solutions, VB and its ecosystem of OCX libraries.

It is more widespread that people think, because when many argue about OOP, they miss the full spectrum of how OOP is approached.

> Actually something like traits (as concept) is what powers COM, WinRT, the basis of Objective-C protocols that influenced interfaces in more common OOP languages.

So? Giving up Rust-Traits doesn't mean that I will give up on interfaces as a whole.

I don't like the way C++ does interfaces, for example, but that doesn't mean that my ideal language won't have interfaces.

Traits seem, to me, a lot like generic functions and methods in CLOS (Common Lisp). It's just that CLOS doesn't bundle a group of generic functions into a trait, they're a la carte.
Yes they do, they are called protocols.
Traits are the one part of Rust that should definitely not be given up. It is way better than the traditional OO model.
> This is why I think we need a high-level, no-BS version of Rust.

F# is probably what you're looking for if you want functional-lite programming with a strong type system, but don't want to deal with the pains caused by static resource management.

It might sound superficial but the simple fact that there is seemingly a hard dependency[0] on Visual Studio (Code) is a major turnoff for me. I am quite attached to my Vim/command line workflow.

[0] https://docs.microsoft.com/en-us/dotnet/fsharp/get-started/i...

(comment deleted)
That link includes documentation for building and running F# projects using only the .net core framework. Seems like the main set of headings is misleading, making it sound like you must install VS or (shudder) VSCode to get F# installed.
F# has some nice things in it: I especially like how it handles multiple return values. But compared to the language it is most clearly decended from, OCaml, it ties you to .NET Core and it lacks an ML-like module system and macros.

If you want to use .NET, F# is a fine choice. But otherwise, and especially in the context of an alternative to Rust, I recommend OCaml.

>This is why I think we need a high-level, no-BS version of Rust.

F# to the rescue. :)

(comment deleted)
>if you use dynamic `Arc`s almost everywhere in your code,

Right but you dont have todo this everywhere, just some sporadic use where it makes life a lot easier. It doesn't have to be all or nothing.

Being able to build on libraries that are efficient means that your inefficient rust can still be lightweight.
> I agree; if you use dynamic `Arc`s almost everywhere in your code, what's the point in a systems PL whose essence is in managing static lifetimes?

You can have the best of both worlds: In many cases, I simply put an object into an Arc, clone that a bunch of times, and pass/store it to wherever it's needed. Then, within loops, I pass a reference to local function calls (instead of cloning the Arc for every call). For any given v: Arc<T>, doing &*v is zero-cost – unlike in many other languages, where you'd have to pass the Arc itself, which would involve atomic increments/decrements without escape analysis.

Vote here for Haskell, which receives great hate on HN because it allows essentially unlimited abstraction.

Meanwhile it has one of the fastest concurrent+GC runtimes, obliterating Ocaml in this regard, and competing handily with JVM or CLR langs.

Finally, it has standard abstractions for concurrency and parallelism that are basically flawless.

Clearly, the hate comes from the association of Haskell with category theory, which is frankly useless/borderline harmful to the working Haskell programmer.

CT was critical to core library devs being able to deliver the main workaday abstractions that make Haskell such an unreasonably productive app languages. But since 2012 you can simply use these main abstractions and enjoy a level of safety and maintainability that rust, c++, ocaml, kotlin, java can't touch.

It's not perfect, but there's been huge improvements of late to tooling (cabal) so there's never been a better time. I really wish the tide could turn, because if you're willing to take on rust for low-level, you're missing out if you don't try haskell for everything else server-side.

I just tell everyone, just write Rust like you'd write Scala. Don't try to optimize the crap out of anything until you need it, especially if no one else is going to use your code.

Just make the allocations.

Even if you write it that way it's still significantly more performant and lightweight than the alternatives without much loss in productivity.

Step one is just clone() everywhere all the time. Then you should cut back AFTER analyzing where all of your memory is being lost to.
Can one really escape clone hell afterwards, though?
I generally go by the principle to borrow if I can without effort, otherwise I think a second of the code is in the critical path or the object is megabytes in size. If neither is the case, I clone without shame. The remaining 0.1% gets my attention (Arc, lifetime annotations, refactoring, or whatever is appropriate).

The important part is perspective. Nobody cares if you clone your command line arguments ten times during startup. As Dijkstra said, optimize the 2% of your program that matter, keep the rest simple

Exactly. Even very experienced people will be easily misled to believe something is performance-sensitive -- computers are ridiculously fast. Like honestly, if it is not a tight loop chances are it seriously doesn't matter what you do. Remember, we write plenty of shell scripts that literally spawn a new process for almost every line, and yet they feel instantaneous.
never allocating memory can be and should be simple.
What language actually makes it simple?
In Zig, we are writing a moderately complex application that calls mmap many times in the first few seconds and never thereafter. We can use every part of the stdlib and most libraries because we can pass in allocators backed by the memory we reserved at startup.
This is very commonly done in C++. My programs do a flurry of mmaps in the first second, then run for, sometimes, years without allocating again.
I was thinking about C and C++ and whilst they allow you to be very careful about allocations, they also give you every opportunity to get it wrong.
We need not act on the opportunity.

All that is actually needed is for it to be more work to get it wrong than to get it right.

> So much of this pain is caused by premature optimization.

Rust is normally used only when high performance is of uttermost importance, so it will always attract people who want to optmise everything.

> Most of the time, the solution is to be less clever

I've done it myself and saw performance degrade, as was expected. That's fine, but by that point you might as well just use another language that has none of the hard problems and end up having very similar performance, which is what I did.

> Rust is normally used only when high performance is of uttermost importance, so it will always attract people who want to optimise everything.

Which is not the way to do things. Profile, then optimize.

I'm writing a metaverse client that's heavily multithreaded and can keep a GPU, a dozen CPUs, and a network connection busy. Only some parts have to go fast. The critical parts are:

* The render loop, which is in its own higher-priority thread.

* Blocking the render loop with locks set during GPU content updating, which is supposed to be done in parallel with rendering.

* JPEG 2000 decoding, which eats up too much time and for which 10x faster decoders are available.

* Strategies for deciding which content to load first.

* Strategies for deciding what doesn't have to be drawn.

Those really matter. The rest is either minor, infrequent, or not on the critical path.

I use Tracy to let me watch and zoom in on where the time goes in each rendered frame. Unless Tracy says performance is a problem, it doesn't need to be optimized.

Coming from gaming industry i think you might want to measure how far you can go with a single threaded rendering. There is limit of content and code that can be a brick wall later. Here is an example from SIGGRAPH 2021 where Activision presents how multithreaded rendering looks like: https://youtu.be/9ublsQNbv6I ps I don't work with Activision its just a public example that illustrates industry practice.
I'm using Rend3/WGPU, where multithreaded rendering is coming, but isn't here yet. Work is underway.[1]

The Rust game dev ecosystem is far enough along for simple games, but not there yet when you need all the performance of which the hardware is capable.

[1] https://www.youtube.com/watch?v=DDG4bcGs7zM

Cool video that matches best practices. Reducing memory footprint is always good and laying out things in memory is also good way to speed things up without changing amount of work.

I have trouble with concept of WGPU. GPUs are complex by themselves to bolt on top any abstraction that is coming from Web. But its just me, its not important since I am not a 3d programmer myself. I am more Engine / CPU optimization guy.

My interest in Rust and this topic is that i would like to see fine grained task parallel systems written in Rust. Instead of systems with separate thread for render that became a bottleneck years ago. I wish you good luck and hope to see a success story about Rust.

We'll see how it works out.

WGPU's API is basically Vulkan. It exists mostly to deal with Apple's Metal. Metal has roughly the same feature set as Vulkan, but Apple just had to Think Different and be incompatible. I'm not supporting the Wasm or Android targets. Android and browsers have a different threading model, and I don't want to deal with that at this stage. Linux/Windows/Mac is enough for now.

Thought for the near future - will VR and AR headgear have threads or something more like the processes with some shared shared memory model from Javascript land?

(That video isn't me, it's the Rend3 dev, who also works on WGPU.)

Gaming solved same problem by opting out of Apple rendering API support. ( ahaha ). Those who have to support mobile can't skip it ofc.

I dont work with VR/AR myself so I dont know.

This fully hits what I experienced.

I'm using Rust since ~2 months. At the beginning I was trying to write my usual C/C++ code and I got completely entangled with lifetimes relationships and the code ended up becoming a mess.

Nowadays as soon as the compiler starts mentioning lifetimes I see that as a warning => I then take a step back and change approach/design => no problems.

I use a little bit of async (I create dedicated variables that are moved to the async functions) and classic multithreading (I use "mpsc" to exchange data between the main & subthreads) and so far I never had a single segfault nor any kind of weird behaviour, which is incredible if compared to some other languages at least for my programming skills :)

Wow, this really speaks to me. I've sunk hundreds of hours in the last few weeks into a tokio-based thing using a bunch of async and ... what a nightmare. It truly is half-baked.

And yet it seems like so many things have tied themselves to this async/tokio mast. It's not a good look for Rust.

>There are almost no active / popular libraries related to network IO that haven't switched over.

To extend this with a slight caveat: for many of those network IO libraries, there's still often a sync alternative. e.g, ureq in place of reqwest works for many use-cases and doesn't bring in an entire tokio runtime for a blocking request. You can find sync DB libraries.

Some other crates have started to catch on and offer them as a feature-enabled adapter (e.g, Sentry does this and can use ureq in the background).

I feel like a real problem, though, is that there's a division of eyeballs across these boundaries. I would chip in to funding work on a "reqwest-non-tokio-adapter" or something that utilizes all the same reqwest types, but avoids Tokio.

(And I like Tokio! I'm basing a new project on it as we speak. I just cringe every time I need to use reqwest::blocking because of something the sync alternatives haven't gotten around to implementing.)

The last thing I would take away from async/await in Rust is that it's "half baked." It's incredibly deeply thought out with years of RFCs, great contribution work that required both low level implementations in nightly and creating extensions to the memory model, and extensive bike shedding and discussion with the community on surface APIs.
Like a lot of things in Rust, it's incredibly well thought out but its implementation just isn't complete to the point that it is usable by other programmers. A lot of Rust libraries and components of the Rust STL are "half baked" in this way.
async/.await in Rust is a perfect example of "code duplication" in the language core. Okay, you have a nice syntax for performing do-notation on futures, but what about iterators, streams, etc? We have generators in nightly and some third-party macros for streams, which sucks.

A proper algebraic effect system could resolve the problem. You can take a look at Koka to see how elegantly it abstracts common control flow patterns using the concept of an algebraic effect.

Algebraic effects are still a research area - no mainstream language has them fully working - but basic Haskell-style higher-kinded types should not be so much of a stretch, and are a necessity if you want reusable libraries for this kind of stuff.
A proper algebraic effect system could resolve the problem.

It could also take as long as the async MVP itself to get off the ground, and you can't know if it wouldn't hit its own snags when deployed at scale. (From long ago, I remember the "non-parametric dropck" RFC as an illustration of a neat concept colliding with reality.)

Languages with mainstream aspirations evolve under greater pressure. I don't know this, but I strongly suspect that async was necessary for Rust to gain acceptance at, say, Amazon. So it couldn't practically have been designed with a wide-open timeframe. The result is what we have; one can uncharitably call it "half-baked" and tut-tut about the sharp edges, but it's workable if you know what to avoid. (But it's so tempting to imagine what could have been, eh?)

Rust is like C++ in the following way: it has many features and a complex type system, but that doesn't mean you should use all its features all the time!

Dancing around with lifetimes can be premature optimization. Yes you can write very efficient code that way but if you find yourself spending tons of time fighting the borrow checker you might be overdoing it.

I tend to use Arc<> a lot in async code. It makes things relatively straightforward and easy to reason about. Mixing lifetimes with async is probably the most confusing thing you can possibly do.

> Rust is like C++ in the following way: it has many features and a complex type system, but that doesn't mean you should use all its features all the time!

... Until third-party libraries push you to use specific features.

I get the async frustration, but keep in mind that there are really only three possible ways of handling this:

(1) Async in the core language and do async I/O.

(2) A fat runtime like Go that implements lightweight concurrency.

(3) Everyone hand-rolls their own implementations of select, kqueue, epoll, etc. loops as well as optimizations like io_uring for every single application.

Rust chose (1) because (2) is off the table as it's a systems language and (3) is more painful and bug-prone than (1).

As for how to implement async: I am having trouble thinking of a significantly better model than Rust and the fact that the Rust community hasn't dramatically improved async shows that a bunch of people who are way smarter than me about languages and type systems are also having a tough time here. Doing async natively in a systems language with virtually no inherent overhead is some seriously difficult stuff.

I personally think the biggest oops with Rust async is the absence of a runtime in the standard library, forcing everyone to pick a third party library and causing fragmentation around which one to use. Tokio seems like the clear winner but having played with both Tokio and Smol I really think the latter is better designed. Tokio does not implement structured concurrency and makes dealing with lifetimes harder. If we used Smol there would be less of a temptation to just throw Arc<> everywhere and less memory leak bugs around forgotten-about tasks.

(For those curious about the latter: Smol JoinHandles abort when dropped while Tokio just forgets about tasks and leaves them running when handles are dropped.)

> Rust is like C++ in the following way: it has many features and a complex type system, but that doesn't mean you should use all its features all the time!

I have come to the same opinion about Scala. Stick to the basics (i.e. the Scala Book[0] and doesn't even have to be all of it) and it is a joy to use.

[0]: https://docs.scala-lang.org/scala3/book/introduction.html

> There are almost no active / popular libraries related to network IO that haven't switched over.

wouldn't Go afficianados say Go is a counterexample right under every Rust programmer's nose?

How is go a counterexample to the rust library situation?
not to the rust library situation, but it made me wonder, since Rust does not even presume a stdlib, if a standard library with green thread aware implementations, as in Go, is possible in Rust.

(i definitely could have worded that better up above, to your point!)

I do wish async would go away and never come back -- in every language. Some days I long for the NMI: non-maskable interrupt.

I have been coding since the late 70s -- the latter half of the last century. After all I have seen, async feels wrong.

I feel you, I really do, but it has it's place. Quite often you really do want to execute multiple different things in the background and wait for all of them to return before proceeding.

In pseudocode:

   1. var logResults = Background writeLogServiceStarted()  // Sent to different machine
   2. var authoResults = Background performAuthorisation()  // Perform by 3rd party
   3. var userSettings = Background getUserSettings(request.currentUser) // Stored in DB
   4. var results = Background executeQuery(request.query, authoResults) // Different DB
   5. var response = Background generateResponse(results, userSettings)
   6. wait (logResults, response)
   7. transmitResponse (response)

The current async/await solution doesn't really make this as clear as the above though: The code is littered with some form of unwrapping/wrapping at every step hiding the actual intention, the call stack is marked as async making it hard to figure out where and when a sync function can make a call, etc.
The JVM guys are working on that with Loom, and, because of its multi-language nature, that can be brought across to many other languages too. Including, oddly, Rust, because Rust compiles with LLVM and GraalVM has a Truffle interpreter for Rust. I doubt anyone would actually want to run an app that way today especially as it's kind of cutting edge stuff and the Rust ecosystem is forcing async anyway, but in principle you could run a non-async Rust server on the JVM with millions of lightweight threads. It'd preserve the safety properties of the language and even the memory layouts, because Truffle doesn't force GC or Java-style memory layouts on the languages it runs. You can even AOT compile stuff but that requires the enterprise edition.
This isn't really an easy async programming issue the author was having. I mostly do server backends I'd do with Java or Node with Rust and its fine.
A naive question: do I have to use async to build a web service or RPC service? If I code in Java, I rarely need to worry about async since a service framework will take care of concurrency. I may throw in thread local and a scheduler here and there to handle some shared context or background tasks, or using some bounded queue to manage some boutique concurrency. Will using Rust be similar? Or I’d have to know all the async as people mentioned in this thread?
No, you don't. Threaded HTTP servers and standard, blocking network clients are available, just not that en-vogue. The very highest performance servers and clients will be async, because threading doesn't get you there, but then again, you (as in: application developer) almost never need that kind of performance in your HTTP stack anyway (this does not make using Rust for such a project pointless in any way).
You don't need async. It provides no performance benefits if you're not using a garbage-collected runtime.
> Async support is incredibly half-baked. It was released as an MVP, but that MVP has not improved notably in almost three years.

As the primary mover of the MVP (who stopped working on Rust shortly after it was launched), I'm really sad to see this. I certainly didn't imagine that 3 years later, none of the next steps past the MVP would have even made it into nightly. I don't want to speculate as to why this is.

I also recommend avoiding async if you don't need. Unfortunately for people who don't need it but do want to do a bit of networking, a huge part of the energy behind Rust is in cloud data plane applications, which do need it.

> As the primary mover of the MVP (who stopped working on Rust shortly after it was launched)

At the risk of rehashing something that has already been discussed to death, did you stop working on Rust because of the difficulty of launching that MVP? I imagine that all of the arguments involved, particularly about things that are prone to bike-shedding like the await syntax, could be exhausting.

Any idea where we should send money to get things moving again? Lack of money is always the biggest problem in open source, right?

FWIW, I only started seriously using Rust in the past year. I quietly watched and waited while the async/await MVP was being developed. I didn't participate in any discussions. On the one hand, that means I didn't exacerbate any exhausting arguments. On the other hand, I wasn't actively supportive either.

That's not why I stopped working on Rust. Given that Amazon, Google, Microsoft and others all employ people to work on Rust, lack of money is certainly not the problem. There has never been more money in Rust development.
Shoulda written it in C maybe.
(comment deleted)
I am fluent in C/C++, Java, Scala. I really wanted to like Rust, but it is hard. The borrow checker is hard and the language api is not intuitive and/or ergonomic.
In my opinion, easy rust is easy, hard rust is hard. A lot of people take for granted how hard some things are to do correctly until they are asked to do them correctly. Not saying this to put you down, we all struggle with this until we get over the hump.

My best advice for loving errr learning rust is put it aside for a few weeks after being annoyed with it. Survey some other language like idk Haskell, then try it again.

If I am allowed to quote myself[1]:

> I have the unsubstantiated theory that experienced developers have a harder time than less experienced developers when learning Rust. You need to forget a lot of constructs that work well enough in the languages you already know because they introduce things that go against the single owner enforcement that Rust has, whereas somebody with less experience will simultaneously accept restrictions as "just the way it is" and not seek out more performant constructs that can be much harder to understand or implement.

> Rust has a curse (it has many, but this one is critical): inefficient code is generally visible. Experienced developers hate to notice that their code is inefficient. They will recoil at seeing Arc<RefCell<T>>, but won't bat an eye at using Python. I know because I have the same instinct! This makes it much harder to learn Rust for experienced developers because they start with the "simple Rust code that will work but is slightly inefficient" and in an effort to improve it they land squarely in parts of the language they haven't yet developed a mental model for.

TL;DR: Keep Calm and Call .clone()

[1]: https://youtu.be/Z6X7Ada0ugE?t=705

I personally believe that Rust is an exceptional language and it’s ahead of its time. Seriously, can you think of any other language with as incredible type system, incredible tooling, incredible performance, incredible package management and general package quality, incredible compilation target support, and practically no few backwards-compatibility quirks?

But Rust is a really bad general-purpose language. Because Rust isn’t a general-purpose language. It’s designed for high-performance, low-memory, safe computing. The only reason it’s even remotely considered “general-purpose” is because of how great it is. Unless you actually need to write something that’s high-performance, low-memory, or safe (or there’s a really important package in Rust that you can’t find anywhere else), stick to something like Swift or Kotlin or Java or TypeScript. Or even JavaScript or Python or Lua if you’re only writing small scripts.

-

Rust abstracts almost nothing about the OS. In order to understand Rust, you need to understand at a low-level how computers actually work and how languages compile. You need to understand the what, why, and how of threads, the stack / heap, I/O, endianness, etc.; and language features dynamic dispatch, references, async runtime, memory management etc.. These language features are all implemented in Rust, but they’re explicit, you have to think about them and choose your implementation.

People say that Rust is hard because of the borrow checker but there’s much more than that: dyn Traits (you have to understand when a trait can and cannot be dyn), Unsized types (and why there are so many type parameters, because everything must be sized), closure traits, multi-threaded communication, what async actually does, etc. Heck, we almost got you to manually choose allocation schemes (they’re default type parameters, which is weird because Rust doesn’t have many implicit defaults).

You often face decisions like

- should this trait be a static / type parameter, or dynamic / Box<dyn Trait>?

- Is this value borrowed or owned or Cow?

- Should this be a unique reference / Box or a shared reference / Rc or a shared-across-threads Arc or not a reference at all?

- How do we control this value and communicate across threads, do we make the whole thing thread-safe or message passing or provide a shared Mutex/AtomicBool?

- How do we manage async, do we set up an async runtime ourselves (and if so how do we configure this runtime), or how do we add integration with existing async runtimes?

- Should this be a Cell / RefCell because we don’t want to or can’t pass a single mutable reference around? And when / how do we dereference the RefCell as to ensure it doesn’t get accessed anywhere else until the dereference is dropped?

- Can we do this safely, aka encode it in a way the compiler understands? And if not, how do we a) do it correctly unsafe and b) make the unsafe code as short as possible?

-

And this explicitness is part of what makes Rust so amazing, because other languages make these decisions for you. But every decision has costs, and either they a) incur higher overhead, or b) lose safety guarantees.

e.g. in JavaScript everything is on one thread, so no data-races but no true parallelism; everything is garbage collected, so no use-after-free or double-free but you get GC spikes; everything is a dynamic boxed reference and types are checked at runtime, so no passing complex types and type parameters everywhere, but calls are much slower and there is no true type-safety.

Or in C++, there are multiple threads with shared memory but you must explicitly avoid data-races; there is no GC overhead but you must explicitly manage allocs/frees; you can choose whether to pass/store things as references or values and there’s a type system, but values can be assigned / casted to the wrong types leading to memory corruption.

Rust is the only mainstream language where ...

Although I agree with almost everything you said, I still feel rust is a general purpose language.

Most programming projects are single thread, not async, and if you aren't juicing for performance, is pretty easy to write. Even in rust.

I think what makes writing rust for general purpose projects hard, is knowing you can do something better that doesn't have to be better.

Yeah I get that Rust tries very hard to be general-purpose. It also has a great ecosystem, has rare features like proper ADTs/macros and no annoying backwards-compatible quirks, and its one of the best languages for WebAssembly. So there are reasons you might want to write something in Rust that doesn't need to be low-overhead.

But even when you're doing everything single-threaded with Rc and RefCell and Box<dyn>, you can still run into pitfalls. More significant, when you do this your code becomes incredibly verbose. You have to constantly specify over and over again you're doing this the high-overhead way and not the weird optimal way, it gets annoying.

Honestly I think Rust should have an "easy mode", where you everything is implicitly wrapped in ARC / dyn / mutex, and when you write code it's almost like you're working in a truly general-purpose "regular" language. Except it interfaces perfectly with regular Rust and compiles to Rust, so you can still use your favorite cargo packages and even jump into real Rust when you need to.

The issue with that is Rust is already stretching itself kind of thin. So this "easy mode" has to be implemented so it's very simple and you can completely ignore it if you want. Maybe it will just be adding better FFI support to Rust, or maybe it will be another ambitious language like Rust but has better support for implicit easiness.

The evolution of JavaScript from "DHTML" to jQuery to "JS: The Good Parts", browser runtime speed improvements and the introduction and development of TypeScript really highlights to me that languages that are complex (e.g. how JS objects work) can be explained, improved on, etc.

I guess what I mean by this is just as C++ got move semantics and auto keywords, it's very possible for a language to choose to evolve in ways that can make code easier to maintain and more expressive. TypeScript or Kotlin are examples of this. Also .NET core and Java since Gradle.

Some things rise in popularity despite their complexity. Objective-C and Swift come to mind... as well as writing truly portable Unix code or Kubernetes.

Personally, I'm waiting for the inevitable "Rust: The Good Parts" that I can feed into my linter and help myself and others on my team understand which parts are minefields of complexity and which parts are incredibly useful once understood.

In JS, there were multiple JS APIs and common practices with global state and more that just fell off the face of the earth. Most IDEs now say what is modern and what isn't, backed with new syntax and conventions that avoid traps in understanding like rebinding this for magic syntax, or using lots of stringly-typed functions.

The only problem I can see is that improvements don't come for free, or quickly. In C++'s case, some of the improvements took multiple decades before they were implemented and half a decade to become widespread and recommended...

>It’s designed for high-performance, low-memory, safe computing.

Meh. If it had been designed for low-memory computing, it wouldn't have had an implicit static global allocator, or a standard library that panics on allocation failures. Currently you have to choose between having no upper bound on your program's memory usage, or using an allocator with an upper bound and accepting that you'll crash on OOM, or giving up on almost the entire third-party crates ecosystem and write with no_std.

The upcoming effort to stabilize std::alloc::Allocator and the A in `Vec<T, A>` etc doesn't help, because every piece of third-party code that currently uses `Vec<T>` is implicitly using `Vec<T, GlobalAlloc>`, and using other allocators in your own code will do nothing to change that. Heck, libstd's own std::io::Read::read_to_end requires a `Vec<u8, GlobalAlloc>`

Maybe someone in the future will invent some bastard child of Zig's comptime, Zig's explicit allocators and everything else from Rust, and that will be the language to rule them all.

I spent about 10 years as a professional C programmer, userland and kernel on Linux, BSD, and WinAPI systems, and I feel like I have a pretty solid grip on "how computers actually work and how languages compile" (I've written compilers with codegen), and "the what, why, and how of threads, the stack / heap, I/O, endianness, etc.", as well as (from C++) "dynamic dispatch, references, memory management etc.." and, like everyone coding in the 2000s, "async runtimes" as well.

Rust is hard. It's not hard because you have to understand all those things. It's hard because you have to understand how Rust understands all those things. Different kettle of fish.

I'm perpetually leery of "you'd have an easier time with Rust if you had a better grip on the CS of systems programming". No, that's not it at all.

(It's fine that Rust is hard. I recognize the achievements, and see where it's a near-perfect fit; I'm looking forward to Rust kernel code. But then, there's a reason almost nothing I write is in-kernel, even when I need "performance".)

tptacek knows whereof he speaks.

Rust has opinions. They are all at least arguably reasonable opinions. But, often enough, you have sound reasons to do things differently. Then, Rust will fight you.

Rust is a pretty good language, and is getting better, but it just takes a very long time for a language to mature. There are no short cuts.

When Rust is mature it will be very far from simple. People will talk about which language subset they are working in. It goes with the territory. Tools that adapt to the real world get as complicated as the world they serve.

I don’t buy it. To write meaningful C that you actually understand requires all the same knowledge of how computers work, yet C is a tremendously simpler language than rust and has an easy to comprehend memory model.

The difference is all the stuff Rust adds on top of fundamentals to make it easier to write correct code with respect to memory usage (in other words, it’s a sophisticated system to make sure you always pair one malloc with one free and don’t make any of the other mistakes C permits partly as a result of its simplicity)

Rust is hard, from a systems programming perspective, not because of anything about the problem domain but because Rust has an unusually narrow perspective of what systems programming entails. And it is unforgiving for software that has requirements outside that perspective, which happens more often than I think some Rust proponents allow.

Modern systems software architectures are thread-per-core, involve a lot of DMA, etc for performance reasons. That is language agnostic, though most is written in C++ because it is amenable to it. This obviates a lot of the safety features Rust obsesses over. It often feels like the Rust community hasn't acknowledged that this is how systems software actually works these days, or the deficiencies of the language in supporting these software architectures.

I started picking up Rust a bit over two years ago, and it was HARD. I didn't have teammates who knew it to help me out, so I used community resources (/r/rust, exercism.io, etc.) I'm still not great with the language, but I'd say I'm about as proficient in it as anything else. The progress is slow enough that you find many more opportunities to quit, but once you do become productive, I think the hard work really pays off.

I've written before about some of my woes in taking a service to prod with mostly Python code. It was easy to write and get running, but then it had all sorts of errors that were basically impossible to detect until runtime. Kind of a nightmare to maintain. Transitioning it to Rust was a lot of work, but I've literally only had one error that I had to fix, which was caused by my faulty assumption about incoming data that I simply `.unwrap()`ped.

I initially was interested in Rust for the promises of higher-performance, lower memory footprint compute, and that is a great aspect of it. But the correctness guarantees that I've experienced make all the hard work I went through absolutely worth it.

Sure, it would be great if the learning curve was less steep. But that doesn't mean the hard work isn't worth it.

Rust and Python sit at almost extreme opposite ends of programming paradigms. The question on the table remains if the excessive complexity of the Rust language is factually worth the pain.
If you need performance, compiled binaries, etc then yea it sure can be worth it. In some cases you don't have a product if you can't get the performance to reach a certain level. In other cases you spend a lot more money tracking bugs in production that rust just doesn't allow to happen in the first place. Every project has different priorities, some projects don't need rust at all, some greatly benefit from it. The complexity is worth it for me, even for hobby projects, mostly because after a few weeks of toiling with it, I learned it wasn't that complicated, and what it gives me in price of mind way surpasses that.
You are basically asserting that performance & correctness are beyond the reach of developers who build systems in other languages. So based on this I suppose all systems developed in other languages are either performing subpar and/or are incorrect?

"Is the pain worth it" is the question. My 42 years of professional development tells me the answer is no. (I lean towards programming pleasure and not pain). YMMV.

I think the idea is that performance and correctness are easier to achieve with rust. Once you get over the initial mountain of learning. The mountain is a different height for each person.
I would say most systems are either slower or less correct, yes. The nice thing about being a developer is that these costs are mostly hidden from us. The business pays for more compute if you churn out slow code, and churning out bugs just increases your job security.

Imagine if you had to pay for the performance loss or production bugs directly though...

For the rare case where the small speedup is otherwise worth the lower producitivty of Rust, it can still be a worse choice because the you have a disadvantage in iteration speed to rework the code for a faster problem solving strategy / algorithm.
On the contrary, I find the speedup due to rust to be quite large and the productivity to be quite acceptable. Refactoring for me is so much easier when I can rely on the compiler to catch so much.
The strong and static type system makes refactoring and iteration pace extremely predictable in Rust. And it's often quick to do as well.

With dynamic languages you can pretend you're done in an hour and then endure a lot of production bugs. That's not being more productive than Rust. That's playing pretend that a complexity doesn't exist.

ocaml though? it's got plenty of gnarly symbols bc obviously we love that shit plus is safe enough and only a modest pain in the ass to write.
While that's true they sit at opposite ends, I find Rust is a really good compiled language to switch to for people used to Python.

For a lot of cases, my rust code is very identical to my Python code when it comes to writing CLI tools or flask like web apps.

lol "excessive complexity". Compared to what? Every language if you learn it to sufficient depth starts to appear that way, and Rust is mostly displacing C++ which is insanely more complex.
I’d agree they’re at opposite ends of the complexity spectrum: it’s very difficult to write a moderately complex python program which uses even a single dependency and is bug free, then deploy it to the machine of a non-technical user. Rust meanwhile excels at this task.

Oh wait, that’s not what you meant?

In Python, "it’s very difficult to write a moderately complex program which uses even a single dependency and is bug free" - whereas in Rust, it's very difficult to write pretty much anything, but if you manage to do it, it has less bugs, (and being a single binary, is easier to install - but you don't need Rust for that)?
>I initially was interested in Rust for the promises of higher-performance, lower memory footprint compute, and that is a great aspect of it. But the correctness guarantees that I've experienced make all the hard work I went through absolutely worth it.

I'd go with 1/3 less performance by using Java or C#. While having better productivity. If we are talking a web service, of course.

As someone that has only dabbled in Rust, the post reminds me of C++ and its mind-boggling templating system. Rust even seems to provide you with the multi-page compile errors.

Is it really as bad, or does the post only highlight the misery you get when you're working on the fringes of what the language is capable of doing?

It's actually really fun once you get the hang of it and realize the compiler is there to help you ship code that runs! This person hit a rough spot and got very frustrated basically... It happens, it's part of learning.

The hardest thing about rust is, it tells you straight up when you don't understand something you are trying to do. For people who are otherwise productive and think they know certain things, this can be an ego slap. In return it makes you a better programmer because you learned something, it's just a bitter pill sometimes.

In my opinion this person should have gone to rust discord/discourse, and asked for help. A lot of this would have been explained. Some of what they dealt with are rough edges that require practice to overcome though. But yea, lots of people are happily shipping async code in rust, right now. It's very possible.

Though most people don't get the hang of it.

Some people also get into C++ templates and build complicated things with them for fun (eg the whole subfield of template metaprogramming came from inventing a clever abuse of the accidental power of templates as a poor man's macro system)

> Rust even seems to provide you with the multi-page compile errors.

rustc strives to provide you as much relevant context as possible. When hitting very verbose output, it is a hint that what you're trying to do is difficult and will require considered design that the compiler can't help you with. The compiler is trying to help you clarify your code so that it can understand it.

Looking at the diagnostics shown in the blogpost, I see only one that is indeed terrible[1], but those in particularly are getting less and less verbose as we tackle them one by one with more targeted diagnostics. I also see the lifetime errors (the closure one when you specify the argument type and the on "`Execute` impl is not general enough"), which I wish gave more context.

As support for HRTBs lands, related errors will stop happening as much and the code will indeed work (or the compiler will be able to tell you what alternative syntax you should be using).

And, in a general plea for people writing Rust, when you encounter a subpar diagnostic, file a ticket[2].

[1]: https://hirrolot.github.io/media/rust-is-hard-or-the-misery-... Be aware that this is not exactly what rustc outputs, as some text decoration have been stripped, but that is a lot of text.

[2]: https://github.com/rust-lang/rust/issues?q=is%3Aissue+is%3Ao...

For me the notable difference is that the Rust compile errors are almost always about the problem I actually have.

They can be quite verbose, but that's largely from showing me what the problem is exactly. e.g. Rustc will show you where you borrowed X and then where you tried to modify it after forgetting it was borrowed, not just go "You can't do that, it's already borrowed" and expect you to figure out where and how or fly into a rage because you're sure you didn't borrow it.

The error[E0308] mismatched type message with a type whose name repeats itself several times is a bad sign, yeah, you probably should not make a type like that. But most of the rest look like helpful Rust errors to me.

To be fair, async (and generators, closures and Iterator) can expand to fairly hairy types with innocuous looking code. A think I do to simplify the output is to rely on `impl Trait` or `Box<dyn Trait>` to erase the type at strategic points, but that's working around my inability to find a way to make rustc figure out whether the full type is relevant or not right now.
C++ templates are an inscrutable nightmare, the reputation is deserved. Rust is a bit different.

Any software engineer that doesn't have a love/hate relationship with C++ templates is lying. On one hand they are extremely opaque and not user friendly, unnecessarily so. On the other hand, mastery of that dark art allows metaprogramming that you could only dream of in other systems languages -- the modern C++ template facility is extremely powerful. The number of C++ software engineers that achieve this level of metaprogramming mastery is quite small. In fairness, the C++ language has been intentionally evolving to make metaprogramming much easier than it used to be, and it has made massive strides in that direction.

Rust lacks the expressiveness of C++ template metaprogramming facilities in significant ways. However, it is plausible that it will gain them eventually. The question is if it is possible to support advanced metaprogramming without the train wreck that is C++. I think it is eminently possible to do better, the question is how long it will take other systems languages to have metaprogramming expressiveness similar to current incarnations of C++.

>mastery of that dark art allows metaprogramming that you could only dream of in other systems languages

Can you give an example for this?

Unreal Engine vs Unity. Unreal Engine has a visual editor that allows the developers to accomplish most task without the need of entering the code editor thanks to a visual scripting language, it mirrors the C++ counterpart 100%. I believe it is not possible to do that in C# (Unity language).
Unity is in the process of achieving parity via Unity Visual Scripting.
Rust supports compile-time metaprogramming via macros.
True until C++17.

With C++17, if constexpr, static_assert and type traits provide some tools to make life better, C++20 concepts improve on that front.

Rust macros curently are harder to debug than what for example Visual C++ offers for template debugging.

We are back to primitive expand macro, like in the early Lisp days.

Is there a tutorial-style (or anything beginner-focused) resource for learning modern C++ that you could recommend? A lot of the learning material is about earlier versions (for understandable reasons), or at least don't seem to reflect C++ as it is today, and the best practices of writing code with the tools it provides.
I would argue that the meta-programming facilities that are in Rust are more expressive and can be used to implement things that are not possible in C++. For example embedded dsl and similar. There is likely some cases where templates can express something that cannot be expressed with generics though.
Do not use async rust when you are learning. It is ok once you are proficient in rust.
Personally I avoided async and macros when I was learning Rust. Perhaps this is an advice I would give to new Rustaceans.
The syntax is just hard.

I tell new programmers to start with either Python or JavaScript. The pay is the same or better, and it's much easier.

I've been programing for almost a decade, I still can't understand lower level languages like Rust and C++.

Why, if you can design a new language, not make something with the simplicity of Python ?

If your going to compile the binary anyway, have the complier figure out the types and optimize.

This is why JavaScript is eating the world. We don't need legions of hardcore software engineers. We need people who use a bit of Python, JavaScript, etc, to make their jobs easier.

The future of programing is easier higher level languages. Rust has some applications, but it's too hard for most

Python can be simple, to the degree it is simple, because it makes so many of your decisions for you.

In Rust the design space is very large. For instance, if your new type is going to refer to some data, should it own the data, should the data be on the heap (in a box), should it carry a safe reference (if so, mutable or not?), should the data be reference counted, or possibly atomic reference counted, or maybe for some reason you need to keep an unsafe raw pointer to the data?

In Python this is simple because the decision is already made for you: your new type keeps (essentially) a reference counted ref to the data it needs. That’s it. That’s all you can do.

That’s great if that decision is always adequate for your application. If you need extra flexibility you’ve got a problem.

> If you're going to compile the binary anyway, have the compiler figure out the types and optimize.

Rust does have type inference. I normally only use types explicitly when I'm collecting from an iterator, or when I want simpler error messages.

> We need people who use a bit of Python, JavaScript, etc, to make their jobs easier.

I am not a programmer, but I use a bit of Rust to make my job easier.

> Rust has some applications, but it's too hard for most

Debugging scripts is usually harder than debugging Rust, in my experience. Being explicit about things has a lot of benefits in the long run.

Async rust isn't pretty.

If you are using async rust, and want to borrow data, use an Arc<T>.

In fact, if you ever use tokio::spawn, you will have to use Arc<T> anyway because it requires 'static lifetime.

True about 'static. Sometimes I think I need an IDE shortcut for `Send + Sync + 'static`.

Like `if err != nil`, you know :))

You can also use Box::leak to obtain 'static references to stuff that lives till the end of the program.
I've been doing a lot of Rust the last two weeks. Like others have said, if you stay away from async, it's not that bad. But I also have plenty of experience in OCaml and Standard ML and Scala, and also C++. Still, I'm finding my C++ experience is more hindrance than help most times.

Async & Tokio is pure hell if you have any kind of shared mutable state. And also it's tough to fight 20-30 years of programming experience with modeling things in an object-oriented fashion; I find tying state & method into structs and using 'self' on methods in OO fashion just makes things far worse when async is involved.

The thing that really bugs me about Rust and I think it's the core of what really confuses many people is that it upends our normal intuitions about scoping. In other languages we're used to thinking of the scope of things in terms of lexical scope at the block/function/module/class level, and inside that scope, anything is game. You can almost visually see it. Usually.

Rust can work this way, but mostly doesn't by default. It's like you were programming in C++ but instead of normal copies and references, almost every single variable use was a std::move. Profoundly unintuitive. I just used this variable, why can't I use it again?!

I think many of the things in the language -- lifetimes and borrows especially -- should have been modeled with a more explicit syntax instead of hiding inside existing standard language features (type parameterizations and assignment)

> Rust can work this way, but mostly doesn't by default. It's like you were programming in C++ but instead of normal copies and references, almost every single variable use was a std::move. Profoundly unintuitive. I just used this variable, why can't I use it again?!

You just need to get used to it, really. As you've noted, it's about our habits. The ownership model makes perfect sense after some experience in Rust, unlike that stuff with asynchronous programming.

couldn’t agree more. I’ve been thinking a bit about rust recently and why it’s hard to write when starting out, and I think needlessly verbose syntax is about 10% of the problem and not enough syntax around borrowing/the memory model is 90% of the problem. In other words, Rust syntax is minimalist in all the wrong places.

A lot of borrow checking related stuff in rust forces you (until you internalize how it works) into a check/compile-fix loop that is horribly inefficient and tedious. if performing moves had special syntax it’d be palpably obvious to even new rust programmers that a move is happening and that assignment does not work as it does in about 99% of other programming languages (in most cases).

also important things that then invert or alter this behavior should also have special syntax. for example, I’m still not sure how any client code is supposed to know that some library type implements Copy (which complicates things further by inverting rusts inversion of typical semantics). seems like the only way is “try it and see” or read the docs. I guess the idea is that you shouldn’t necessarily need to know this, but I do think rust would have benefited from making borrow and memory handling more transparent and obvious with special syntax around it (beyond lifetimes, at point of use, not just in signatures). As it stands it seems to want to give users the control that comes with manual memory management with the sort of “hiding away” of details that GCs traditionally afford

>Rust can work this way, but mostly doesn't by default. It's like you were programming in C++ but instead of normal copies and references, almost every single variable use was a std::move. Profoundly unintuitive. I just used this variable, why can't I use it again?!

This was my experience of rust as well. I've since forgotten exactly what I was trying to do (it was a year ago now), but a seemingly innocuous check of the variable I was using meant I couldn't use it immediately afterwards. I have no issues with move semantics in C++, but making that the default behavior in rust made it a pain in the ass to use, especially when it doesn't seem to be consistent. I ended up rewriting a week's worth of rust in C++, including the unit tests, over two days.

Yeah to be clear I'm not opposed to the semantics of how Rust manages lifetimes. I'm ... disturbed.. by the syntax.

It feels "bolted on".

Variable assignments and usages that borrow should be clear with a different syntax.

Lifetimes should not be lumped in the same box with other type parameters but instead broken out visually into some other sort of annotation on the type.

BTW I also think "std::move" in C++ is a hack. They should have introduced a new syntactical element for that as well instead of masquerading it underneath a pseudo-function.

I've been doing async Rust since the tokio 0.x combinator days, and I remember being like OP. For some reason `Arc` did not exist in my mind and it was a struggle appeasing the borrow checker.

The Go version is similar to the final Rust version; except the Go version is forcing you to use Arc everyone[1]. Seriously, just use Arc (or Arc<Mutex<>>), in 70% of cases you are wrestling with the borrow checker trying to do something dangerous, in the other 30% the borrow checker is wrong and isn't smart enough to understand what you are trying to do. In both cases, 95% of the time, you aren't creating enough objects per second to justify getting rid of that atomic add.

I'm not even sure this is limited to async code either. I've seen plenty of code by C++ "zero cost" gods which abuse lifetimes in order to avoid a single clone or Arc. At least the compiler made sure you won't segfault, but I'm not sure the complexity is worth it over just an Arc.

[1] I know this this is a simplification.

I do completely agree with your advice of using `Arc`s. But unfortunately, sometimes it gets very tedious to deal with all these `Arc`s: https://github.com/teloxide/teloxide/blob/ec1d41220c51872cf9....

It also blurs the advantage of Rust as a lifetime validation tool if you use reference counting anyways. But it seems that it's the only viable approach for async at the moment.

I have plenty of code like that:

    let id2 = id.clone();
    let watch_id2 = watch_id.clone();
    let id3 = id.clone();
    let watch_id3 = watch_id.clone();
    let ready2 = Arc::downgrade(&ready);
    let video_stream = async move {
        //  use id2, watch_id2, id3, watch_id3, ready2
    }
My realization is that in most cases an Arc is the right tool for the job, especially in concurrent applications. I see it as the borrow checker doing it's job, if you have two threads that need to share data and one of them may go away at any moment, the only way you can ensure the memory stays around is with "runtime borrow checking", e.g. a garbage collector.

In C++, the way this is solved is usually "just trust me, I won't use this memory here"

The overall effect of Arcs being more usable is something that was true before Rust. It is an emergent property of Rust too, because it's still true. The alternative to using Arc or similar used to be validating your own lifetimes manually and inevitably making show-stopping mistakes in C/C++. Now the alternative is machine-checking it and being able to never make mistakes. But Arc is still more ergonomic than either.

If Rust forces you to use Arc where in C/C++ you would normally just be freewheeling your lifetimes, by complaining about your lifetimes, a lot of people are going to see that as "Rust is really annoying and gets in my way, it's making me choose between a tangle of lifetime bounds and explicit Arcs". The truth is that it is teaching you that your normal style would have led to mistakes and you should never have been coding that way. So people go on complaining about being re-taught until their normal style works in Rust, or until they give up. (I don't think that's what you're doing, this is mainly just to say Arcs are not a waste of the language's power, rather an effect of the language's power to know when your freewheeling would be mere lifetime guesstimation and quite possible buggy in C++.)

The author's point is strong though.

> the process of designing APIs is affected by numerous arbitrary language limitations like those we have seen so far.

There's a long way to go in being able to perfectly express all the ways you might want to use lifetimes. Sometimes you come across a situation when a few of these limitations converge on one spot, and you get the author's problem. But equally, the author is trying to write something that has probably never had its lifetime rules described before. Yes, languages with GCs can do it easily. That was also true of lock-free queues & running destructors. GCs have it easy in a ton of different areas around lifetimes. The core complaint here is that Rust does not have a GC and we have had it really easy being able to rely on one & not having to write down exactly the lifetime bounds that could make it work statically. The author is complaining that there are hundreds of compiler issues preventing some of this being expressed... well, yeah, nobody has ever, EVER, tried to write this stuff down before in a machine-readable way, we are slowly covering the untrodden ground, and you might be in an especially untrodden area. I think that's a pretty good excuse for the compiler having issues and some things being not quite expressible, but it's also an important caveat to "fearless concurrency" and I think the post illustrated that well.

"If you can't handle me at my BoxFuture, Arc<Mutex<_>> and #[async_trait], you don't deserve me at my zero cost abstractions and fearless concurrency."
> except the Go version is forcing you to use Arc everyone

The overhead of atomic reference counts everywhere is higher than the overhead of a good garbage collector. This thinking is biased against Go for no reason.

>[1] I know that this is a simplification.

I don't think it's biased against Go. The an `Arc` (or garbage collector) is the correct way to model lifetimes for complex programs whose lifetimes are managed at runtime rather than compile time. In the context of both languages, rarely is overhead of an atomic add or GC going to be the limiting factor of your programs, just like Java programs are efficient. While static memory management is nice, it's not the sole reason I continue to use Rust. I've been using Go since 1.0, and there are two times where I have ever thought "My program is slow because of GC", and in one of those times, it was solved by simply upgrading Go.

If you want a better C++ that lets the compiler do work for you rather than you working for it try D.
While I can see why zero-cost abstractions are important in Rust, I was wondering how much simpler Rust async could have been if zero-cost had not been a hard requirement. The way I see it, Rust async is most useful for IO-bound applications, which can afford some non-zero abstraction CPU cost.
If zero-cost wasn't an aim then it would indeed be much easier. For example, async fn in traits isn't (yet) in the language. The type system level changes to make them possible is being actively worked on (it is high priority!), but if we didn't care about "you can't write more efficient code by hand", then we would have provided that feature with the semantics of the async-trait crate[1], which provides a macro that turns `async fn foo() -> i32 { 42 }` into `fn foo() -> BoxFuture<Output = i32> { Box::new(async { 42 }) }`. Going down that route would have "silenced" that criticism, at the cost of locking us to subpar behavior at least until the next edition. The approach of waiting until things are baked well enough to land in stable is frustrating as a user today, but I believe has a better chance of standing up to scrutiny long term. In the meantime, the community is able to "plug" those failings, at the cost of worse dev UX due to not being builtin.

[1]: https://crates.io/crates/async-trait

I'm very confused. Rust is mainstream? C, C++, Java, C#, Python, JavaScript are all what I would call mainstream. Rust is not a language that comes to mind at all.
Rust has hit the point where it's in the conversation for what language you'd write a new project in, even for big companies (typically the most lethargic). I'd say it's at the level of mainstream where there's Rust being written in a significant portion of the industry, but not at the level of mainstream where it's somewhere in the stack behind most of organised society
Pretty damn mainstream nowadays, yes. Quite a few big tech companies are either actively doing much of their new systems programming in Rust, or else dipping their toes in.

I honestly don't know of a big tech company that is happy with the idea of just continuing to use C++ indefinitely - they are _all_ looking for alternatives, and Rust is the most obvious option.

> I honestly don't know of a big tech company that is happy with the idea of just continuing to use C++ indefinitely

Every game company ever. And Google.

The Google team I work on (ChromeOS, crosvm[0] specifically) has been transitioning to Rust for a lot of our new services (mostly crosvm and stuff that interacts with it) and I couldn't be happier :)

[0] https://google.github.io/crosvm/

The claim was "continuing to use C++ indefinitely." For that not to be true they'd have to be looking at replacing C++ entirely. Moving everything to rust/go/whatever. Do you foresee Google doing that?
Google as a company? Not necessarily, I don't have a crystal ball.

ChromeOS as a product? Maybe. (note: not Chrome)

ChromeOS platform internals? I totally could. You don't just rewrite everything from C++ to Rust, because that wouldn't make much sense, but more and more new service/products are being spun up that use Rust (we have docs and stuff https://chromium.googlesource.com/chromiumos/docs/+/master/r...).

> Quite a few big tech companies are either actively doing much of their new systems programming in Rust, or else dipping their toes in

I don't think that's true. It seems to me that a lot of people believe this simply because a lot of other people believe it. You see it everywhere. "Oracle rewriting MySQL in Rust". "Microsoft rewrites Skype in Rust". "Linus Torvalds rewriting Linux in Rust". I have yet to see proof of any of it.

It is true. I’m not basing my statements off random info I gleaned from the internet.
> I have yet to see proof of any of it.

It looks like you're actively avoiding finding that evidence.

Citing clickbait titles doesn't sound like you have actually looked.

Just checked Rust job offers on Linkedin in my country (France): 338.

C++: 27 172

More people pick up coding C++ professionally in any given week than the total being paid to code Rust full time.

The bigger a company is, the more various languages people there dabble in, and the less it means that somebody there dabbles in your favorite. You would better add up revenue (NB: not market cap) of companies specializing in using your favorite language; but that number would be disappointingly small for any language as far from maturity as Rust still is.

I wasn't aware that restating an entrenched state of affairs is an argument in a discussion.

Doubly so when that entrenched state of affairs is actively being attacked every day for years now.

But sure, let me not stop you.

The article mentions:

> It compiles fine now but the final API is defected: ideally, we do not want a user to wrap each handler with Box::pin.

Why not, whats the problem (apart from some sort of philosophical idea of "beautiful api")?

The author is 17 and writing code (and posts) at this level? The kids are all right.
Some people are just so ahead of the average. I thought I was pretty good knowing the basics of Rails/Databases at that age, but I was no where near the level of writing coherent posts on languages.
> The author is 17 and writing code (and posts) at this level? The kids are all right.

He is a talented kid from Kazakhstan. So nice to see this here after all that Borat nonsense badmouthing Kazakhstan just for cheap gigs.

This is not a critique of rust but my experience is that I needed to build something that was distributed cross platform as a statically linked binary. So, I narrowed my choices to Golang and Rust. I wanted to learn rust and was only so/so on golang and really tried. I knew this project would go slower because I’d be picking up a new language but as I was still being brain hurt by rust I peeked at go and it can get a little convoluted but overall it’s a readable language where I felt like I could start writing decent code within days.

Since it was mostly about building something for users and not just my own learning I had to go with go.

Not sure if I will ever learn rust but it would have to be for a hobby project or solve a problem for me that is fundamental to the desired outcome that go, python etc could not.