Another data point: After writing / (re)writing 20K+ lines of code as a CLI side project in Rust (without touching async), I think I can say it's the best language for me after 20+ years of experience in other languages. I like the compiler, and I learn a lot from clippy.
The "hard" part about Rust is you have to "unlearn" some of the basic mechanisms like scope and ownership that you bring from other languages. It doesn't allow you to build quick and easy solutions that allows you to lie to yourself (or your boss.) You have to spend more time upfront to compile and fix all warnings, but in my experience so far, fixing these at the right time (i.e. before compilation) is better than fixing them after someone writes a Github issue. I can't say it eliminates all bugs, but my trust level to my Rust code is around 10x more than my Python code for the same problem. (And I'm writing Python since 2002.)
The pain comes from async. Over time, I came up to this conclusion: if someone tells me that Rust is nice and you only need to change your mind, this person doesn't write async code. Or he/she writes very-very straightforward, if not primitive async code and doesn't touch HOFs, traits, and similar stuff at all.
However, when you write networking code, you typically use async. The worst role is being an async library author (me).
People talk about C++ suffering from its commitment to zero-cost abstraction, but the same thing applies to Rust async. While async may theoretically be the fastest possible way to write asynchronous code, it feels like an order of magnitude more painful than the CSP/channel-based approached used in languages like Go and Clojure (and the upcoming Java Loom).
Personally if I had to write async code that required anything other than the absolute minimum possible latency, I'd prefer to write Go, and I say that as someone who thinks Go's lack of generics was an absolutely terrible idea.
I made the mistake of trying to learn Rust while doing async programming.
IMO, when it comes to concurrent, it's a matter of picking your poison:
Threaded Rust: No overhead of a GC, but overhead of context switches and multiple stacks.
NodeJS: No overhead of context switches and multiple stacks, but the overhead of a highly optimized GC. (And I suspect that the GC can do tricks like run when the process is waiting on all tasks.)
Only if your application can limit the number of threads to the number of physical cores.
IE, of you're doing a web server with a thread or process for each incoming web request, you're blocking and context switching. If you have to have locks, you're also blocking and context switching.
This is why async programming models are common, they move the logic of blocking and context switching into the language and runtime, where the compiler can juggle more concurrent tasks in a single thread. It's just harder to do in Rust because, to oversimplify, things that are in stack memory in a threaded environment are now on the heap. In C#/NodeJS, this difference is transparent, but in Rust it's not.
Async in something like C# is much less painful precisely because it doesn't try to be a zero-cost (or at least as low cost as possible) abstraction. When the language can allocate stack frames on the heap implicitly as needed, and there's GC to clean them up, things "just work".
Go now has generics. The performance of using them is hit or miss it seems.
Your opinion is valid, but I would say, if you aren't juicing for the best performance, you can adopt easier patterns to async. There are comments on this post detailing how to go about doing that. Or yea use another language if you want.
Can I do this without having to wrap half the libraries in the ecosystem if I want to use them without worrying about async? C# has that issue: the ecosystem buys heavily into async, so it can be hard to avoid it.
On embededded, you can write straightforward asynchronous code without using *Async*. You do it using DMA, and interrupts, perhaps with static analysis etc. There are efforts to use Async on embedded rust to abstract these, but it's not required.
Of note re networking: My observation is that the Rust Async ecosystem only covers TCP and higher. There are loads of Async TCP, HTTP etc libs, but nothing that can do anything lower than that! At that point, you're looking at perhaps `socket2`, and `smoltcp`; the latter, of note, also works on embedded, and goes lower than TCP, despite its name.
My best guess is that a lot of people are using Rust for TCP and HTTP level web programming, eg servers, where spawning 100s or more IO-bound processes at once makes sense; the area where Async shines. Why I'm confounded:
#1: Rust excels at low-level programming; ie it's one of a select group capable of this (Along with C, C++, ADA, and zig)
#2: Web application programming in Rust has a long way to go to get to the level of Django and Rails. It only has Flask analogues.
Neither of those goals are served by the existing Async-based ecosystem; it occupies a spot in between.
> The pain comes from async. Over time, I came up to this conclusion: if someone tells me that Rust is nice and you only need to change your mind, this person doesn't write async code.
Async code is a pain in almost any language. Certainly any language that differentiates between async code and non-async code has the async code be a pain.
> Certainly any language that differentiates between async code and non-async code has the async code be a pain.
Function colouring is not the only problem with async code. The difference is that concurrency in other high-level languages usually don't break down polymorphism and other language features. Also, they don't push you to deal with lifetimes, which is a serious issue in Rusty async.
Writing async code in C# is a lot easier to me than in Rust. Unfortunately, I didn't have a chance to write async in functional languages, such as Haskell or F#, because they are well-known for elegant concurrency.
I'm not sure if C# async was derived from F#, but it definitely looks very similar. The main difference is that in F# async is vastly more customizable (but also slower, because the compiler can't make certain assumptions due to said customizability.
Writing this kind of async code in Haskell (and to some extent OCaml) is much nicer, because you can abstract over the asyncness of code. This can't be done in Rust or C# because the type system isn't powerful enough (no higher-kinded types). To be fair, adding HKTs to Rust's existing type system is a challenging theoretical problem in itself.
In a sense, but the "ill-formed, no diagnostic required" hack allows for scenarios where what you wrote is nonsense, and a human can explain why, but your C++ compiler doesn't have that insight, so it compiles anyway and does... something. This avoids needing to teach the machine how to determine if what you did was sound.
But this is of course not a very safe way to write software.
If the author is fool enough to not use the language features that exist since C++17 to validate template code, surely.
I also don't find debugging Rust macros that fun, yet most likely the answer will be that the macro author didn't took enough care, and Rust is great to write gigantic DSL macros.
> If the author is fool enough to not use the language features that exist since C++17 to validate template code, surely.
To be sure the fact the diagnostics aren't required does not forbid them from being provided, but it does mean you'd need to know whether you've been provided with such diagnostics and how effective they actually are. Unless the answer is "I have diagnostics and they are 100% effective" you're in the same situation.
> I also don't find debugging Rust macros that fun
Which kind? I don't find debugging the declarative macros too hard, they are after all just expanding what you wrote according to some simple rules, and you can ask the compiler to show that expansion to you.
Procedural macros present unlimited potential for exciting debugging because now you're essentially modifying the compiler at runtime. A C++ pre-processor macro can cause some nasty problems but it's not going to run a different compiler... [Technically Mara's nightly-crimes only runs the same compiler with different flags, but it could run a different one if she'd needed to do that]
You might be right on this. I have written a lot of rust and a fair bit of network code, but I never went near async. I'm not exactly sure what my problem with it is, but I started writing rust before async was a thing and always felt more comfortable with finite state machines and polling for network code (I guess I am atypical).
1. Async for trivial things is straightforward and easy
2. Rust actively discourages some async patterns to protect you from some memory misuse edge cases
It does limit your freedom in writing code that eg. relies a lot on async callbacks, but there's a reason.
The first time I did a massive async project (think a rust binary maxing all cores executing the largest possible number of async fns doing various things in parallel - from fetching data online to running tensorflow) I came at it all wrong and wrote something that would have worked in node or haskell but that was a pain to compile in rust.
After days of pain I understood what rust wanted and nowadays I use the same pattern and it's fairly easy for me. Just another tool in the shed.
I disagree. I write Rust, I write mainly async Rust and while my code is maybe not the most sophisticated I use a lot of async traits and generics. I still find it one of the best languages I know, definitely my favourite for all around coding at the moment.
That's probably true. I've experimented a bit with tokio for local IO bound tasks, but decided it's unnecessarily complex. I think some patterns will emerge from current Rust async development eventually. Good parts will be easier and bad parts will be removed, but digestion of such concepts usually require time.
IMHO the time for doing analysis like this is before submitting code, not before compiling it.
Ideally you want code without unused variables, implicit type casts, ... in a repository. But when you are locally testing out code you're in progress of writing, it is very unproductive if you have to care about unused variables because you're commenting out one line to see the difference, or change casts everywhere because you change the type something temporarily. It'd be nice to only do the work of cleaning up such issues in the final version of your change.
These checks are enabled by default in many environments, build tools or scripts for them etc..., it's not trivial to disable it or requires full recompile.
So I wish a language or build environment would use the concept of "development time" vs "submitting time" for different sets of warnings-as-errors.
This is one recent change in Zig which really annoys me: unused variables are now errors. Languages that complain every time there's a unused variable become useless during experimentation and quick hacking. They turn my hyperfocus into a death from a thousand paper cuts.
I hate it with a passion. Please respect my mental flow, if I'm trying some ideas out, it's just rude to stop me in my tracks to tell me I forgot to comment out a variable. Who cares.
I will fight anyone who thinks this is a good idea.
For a variety of reasons, Zig doesn't do warnings.
Personally I don't get the big deal with unused variables being errors, from either direction. I'm not sure what it accomplishes to make them errors and I'm not sure why people complain so much about having to comment them out.
If you comment out a statement, you cannot know for sure how many unused variables this caused unless you visually scan the entire previous code, which can cost time.
So the only way to find out is to compile and then have the compiler tell you that it refuses to continue because there's an unused variable at line X.
So now you needed to do not one but 2 compiles, to do something that should have taken 1 compile, which also costs time.
But it's then also possible that due to commenting out line X, there's now yet another unused variable (or more) somewhere. Etc... (Unless the language offers a way to fake-use the variable, like (void) cast in C++. Then at least you only have one wasted compile and not recursively more)
And then if you want to enable the original line that you commented out again, you need to also remember to uncomment out those other lines.
Repeat this process many times a day when really in progress of developing something, and due to the total time it costs for this silliness it's just a bad tool, not a good tool for efficient programming in the flow.
The goal of the unused variable as error is to prevent bad submitted code, but that should not cost you time during development. Only enforce that check at the end, not during.
I think that one thing that we really need to revisit in PLs is sorting out different kinds of comments. Like, in most languages, a comment is just whitespace, with no semantic distinctions. More recently, docstrings got some special handling. But what's really needed is a kind of comment that's specifically about commenting out code; ideally, working on syntax tree level (so you can comment out one entire {...} block, say), and with the compiler being aware that the stuff inside is code, and handling it accordingly for purposes such as these.
Earlier in Rust, I saw people recommending `#![deny(warnings)]` (always turn warnings into errors) but I think the community has shifted since then because I feel like I don't see that as much and instead see people disabling warnings in CI.
Speaking of warnings, something I appreciate about Rust is you only see warnings for your own code and not for your dependencies so you can crank up the warnings to whatever level you want without being blocked by dependencies (minus macros). Granted, there could be times where looking at high risk warnings for dependencies could be important.
I'm using cargo-limit crate for "cargo lcheck", "cargo ltest" or "cargo lclippy" to show errors before warnings before clippy warnings. You're right that "cargo check" gives the same priority to warnings as errors, and it's annoying.
I have found that as I gain experience with rust, I spend less and less time “upfront to compile and fix all warnings”. I think rust requires a different design philosophy. It just takes time to adopt it into your mind.
That's true for me as well, but I learned these after many failures. Now, I start by writing enums and structs for the problem, then iterate the design with functions and if it's really needed add these functions as impl of a certain struct/enum. This is the inverse of "Object-Oriented" design where one has to start from interfaces and manipulate data to satisfy the interfaces.
I wrote servers in Go as well, and I like the language. After Rust, I believe at some point, I enjoy to solve the "problems" Rust brings, not that it's rationally better than Go.
using c++17/c++20 with g++'s sanitizers(e.g. undefined behavior) and static analyzers, along with clangd the LSP, they too caught most if not all coding errors at editing time and compiling time. In recent two years once my c++ program compiles warning free, it seems bug-free at the same time.
In my experience this is untrue... I've worked with C++ on and off (albeit, sometimes reluctantly, so maybe I'm projecting some misery) and a lot of errors in C++ can not be caught until it starts pasting the templates. Sometimes it also fails when linking. Sure, you can always go through the whole build/compile process, but for serious C++ projects that's impractical (even with stuff like (s)ccache) because of how long it takes. And I'm a code quality tooling/intellisense freak to the point it can be annoying for others, and I've never seen a truckload of tooling come even close to simply running "cargo check"
I've only experienced waiting ~10 minutes at most (on decent dev machine) and it frustrated me to no end, but even more serious projects sometimes take hours. Maybe concepts help with this?
And the "jUsT dOnT uSe HaLf tHe LaNgUaGe" argument holds as much as a leaky bucket, because it's hard to agree on what the good parts are in a large team, unless its strictly enforced and audited somehow
I tried rust on and off, since I do not need it for work I never had time to fully dive into it. The news about rust on the web just seems too good to be true, are there any "cons" against modern c++?
yes the cargo and tools etc are great, but I can set up a full c++ build env quickly too, not as good but enough for daily coding.
A big con is that the ecosystem is young still. You can generally find stuff for almost anything at this point (even enterprisey stuff like OData client generators), but they're not "established" the same way their C++ equivalents would be.
GUI and Game dev spaces in Rust are very inovativne and in how they solve some problems, and I'm fairly enthusiastic about both spaces, but I'd be reluctant to recommend them for production, because they're still immature/developing compared to anything in C++
Unless you change header files deeply engrained into your code base (or your entire code is templates), incremental builds should not reach 10 minutes, not to mention hours...
That's cool. I've worked in C++11 most recently (in 2012 with OpenCV) and used Boost's smart pointers to achieve memory security but I was probably overdoing those at that time and don't remember it as a good experience. (I remember I spent nights with valgrind but it may be my confusion at that time and don't remember the details.) I'll take a look at these as well to brush up my knowledge on modern features of "grand old language." Thank you.
To be honest that doesn't seem too convincing for a new language. It actually reminds me of the early dogmatism of Java which today is often seen as too restrictive.
Rust makes an argument about safety but only for a select few types of errors and the cost seem rather high.
It brought a discipline to my memory/variable management and functional problem solving, as Haskell brought a discipline in my typeful thinking some time ago. I don't claim it's "the best language for all use cases" but I'm glad I'm learning it.
That's true, though in my experience those quick solutions lead to more quick solutions, and in total that leads to a mess in the long run. But if that long run will never come, it may be better not to bother. These are all sensible choices.
The "hard" part about Rust is you have to "unlearn" some of the basic mechanisms like scope and ownership that you bring from other languages. It doesn't allow you to build quick and easy solutions that allows you to lie to yourself (or your boss.) You have to spend more time upfront to compile and fix all warnings, but in my experience so far, fixing these at the right time (i.e. before compilation) is better than fixing them after someone writes a Github issue. I can't say it eliminates all bugs, but my trust level to my Rust code is around 10x more than my Python code for the same problem. (And I'm writing Python since 2002.)