Things I hate about Rust, redux
blog.yossarian.net
blog.yossarian.net
The number one problem Rust has now is that it’s overall not significantly better than C++ and sometimes the reverse is true. I’ve learned for example quite a good chunk of Rust, implemented a project (actually a rewrite from C++) and now I’ve mostly reverted to C++ and Go. The only reason that I’d use Rust is if I had to ensure that a program is memory safe (e.g. it’s planned to be handed over to developers unexperienced with C++, it will be heavily changed in the near future, etc).
It’s no coincidence that I’ve only learned a chunk of Rust - I skipped async, concurrency and parts of the standard library which I didn’t need. The language and the ecosystem are big and complex, very comparable to the complexity of C++. It’s in a different difficulty league compared to C or Go and I wouldn’t say that it’s hard per se to learn, as the concepts themselves are similar to C++, but there’s a lot to study, remember and categorize and map in relation to how things are done in e.g. C++. One exception is Rust’s equivalent to C++ template-heavy code which is simply not worth wasting one’s life on. Whoever writes that is either a library developer or an asshat.
Compile times are too long and what makes it worse is that everything seems to be distributed as source and has to be compiled. Because cargo is so convenient to use, dependencies are completely out of control: adding a single crate can end up pulling in dozens of dependencies. In my case I ended up with ~150 for ~5 crates!
All in all the benefits barely outweigh the costs for me, especially since I know C++ well. Rust to me is a language for large-scale or low-level projects that need solid performance and provable memory safety, but it’s overkill for many things where one can use Go or a simplified dialect of C++.
Maybe we are coming at this from different angles but I disagree. I remember seeing some discussions in the C++ community about challenges with using `std::string_view` that seemed like they were discouraging it and thinking to myself "this isn't a problem in Rust".
In fact, I refactored a crate I maintain from cloning objects all over the place to using references. I looked at what i did with my C++ hat on and realized that I'd consider myself irresponsible to do the same in C++ without a *very* high bar to justify it, not just because of the cost of the global analysis to ensure it was safe but the fact that that global analysis would need to be repeated on every subsequent change. Being in Rust, it compiled and I knew I was good. I even had an idea for reducing even more allocations that I thought was safe (20+ years of C++ experience, 10 in life-or-death mission-critical software) and the compiler found a case where I was wrong and the approach doesn't work.
Generally Rust does memory safety better than C++, that’s painfully clear as soon as one uses the language and compares it with what happens in a C++ project with average developers. But memory safety is just one dimension of evaluation.
I think this is only true if you're already familiar with C-family languages (C and C++). As someone who came to lower-level programming with a JavaScript/Python/PHP background, I found Rust significantly easier to learn than either C or C++ (C was easy to do hello world, but jumped to WTF-level complexity as soon as I needed to interact with a 3-party library).
Let me put it this way: would you trust a developer who was new to C or C++ to write secure code that's free of memory safety issues, undefined behaviour and multi-threading race conditions? You can do that with Rust, and IMO that's a huge win.
This comes up a lot. A lot of people learn C or C++ in school, so the distinction between learning those languages and learning programming in general gets blurry. Being full of UB that "works on my box" also makes things blurry: If you don't know how to write sound programs, have you actually "learned" the language?
I think learning C or C++ takes 2-5 years. Many people forget that, because it was mixed in with learning other things. Many others quit the learning process partway through without realizing it. Rust forces you to actually learn Rust to write Rust, which can make it seem like there's more to learn by comparison. But if you restrict yourself to the safe subset, I think there's actually much less to learn.
Some examples:
- No DRY between headers and source files
- Arrays are explicitly described as such in type signatures, ie not as pointers to the type they contain
- No pre-processor
- Cleaner, more explicit pattern matching syntax
- No prefixing all constants/structs/enums with the module name
- A struct or enum is declared once, with its name; ie no _t, _e, _s suffix and name duplication
- Type inference
- Explicit handling of enum variantsIf I recall correctly, C# didn’t ship with one but it was added. For one it makes it easier to use the standard build system (just compile all the files referenced) rather than have to use an outside build system that knows when to include linux.xxx and when to incluide windows.xxx etc.
Of course I’m aware of some of the drawbacks but I’d prefer with than without.
- use std::array and std::vector
- Most stuff can be use with templates and constexpr
- use namespaces
- just like in C++
- auto type inference
- enum classes
It is about time to replace C with C++.
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p239...
Type inference is powerful enough with generics being Turing complete.
https://github.com/pjmlp/RaytracingWeekend-CPP/blob/main/One...
This might be true for your specific use cases, but I've worked on reasonably large projects in both Rust and C++ and the fact that I don't have to chase various issues related to use-after-free, returning local variables, dealing with external libraries or general metaprogramming is definitely causing me to mark Rust as significantly better.
As always, if you're a C++ expert you'll bound to disagree, YMMV etc.
I think this is why you don't think it's significantly better. I use JS and Python for most of my work, but when I want something that must do threading correctly or is performance bound, then I go to Rust over C++. That level of safety is the critical value for me.
(I also find it significantly easier to compile Rust than the typical C++ build chain, especially when doing cross platform work, but that's more a compiler than language issue.)
Being able to share code easily is a good thing, and it's one of the biggest draws of Rust compared to C++. You will see the same thing once C++ modules and package managers take off; in fact, if you don't, that means that C++ modules have failed.
Username is definitely relevant! http://www.paulgraham.com/avg.html
> and what makes it worse is that everything seems to be distributed as source and has to be compiled.
You can distribute a binary shared library written in Rust, but make sure you're using a C-style public interface to preserve ABI stability and make it accessible to code written other languages. It's generally feasible to design such an interface and build Rust-friendly wrapper code that can be included in client projects - there are crates that will specifically make this easier. C++ punts on the whole ABI issue and is now facing ecosystem-wide breakage as a result.
Other problems with this:
- You have to link each test file which can be slow
- You get less parallel test running because you've broken things up into smaller parallel batches. cargo-nextest is a proof-of-concept that shows this problem [0]
- Fixture initialization reuse becomes trickier
Projects like cargo explicitly combine all integration tests into a single binary. I'd love it if we explored implicitly rolling everything up into a single test binary except for any explicit test targets defined by the user.
Using Mold [0] as the linker might help. Written by the author of lld.
Program (linker output size): GNU gold | LLVM lld | mold
Chrome 96 (1.89 GiB): 53.86s | 11.74s | 2.21s
Clang 13 (3.18 GiB): 64.12s | 5.82s | 2.90s
Firefox 89 libxul (1.64 GiB): 32.95s | 6.80s | 1.42s
[0] https://github.com/rui314/moldedit: add citation, info, times, link, will -> might, and formatting.
I got a new workstation recently with a good PCIe 4.0 NVMe, maybe I get around to retry and compared with that.
Otherwise, seems like a good list, thanks for sharing @woodruffw.
What do you hate about X is a useful interview question, with discussion of the effect, and alternative formulations that might have saved it.
I struggling to think of any language that can provide this guarantee. Even Python can segfault while executing C extensions. Maybe java?
import sys
sys.setrecursionlimit(1048576)
def recurse(): recurse()
recurse() import ctypes
ctypes.string_at(0)"No_Exceptions - Raise_statements and exception_handlers are not allowed. No language-defined run-time checks are generated; however, a run-time check performed automatically by the hardware is permitted."
Source: http://www.ada-auth.org/standards/12rm/html/RM-H-4.html
If the underlying operating system or board support system have similar support then panic situations would be avoided entirely.
While this is a highly-specialized case, it is defined and supported by high integrity Ada implementations / runtimes.
https://www.adaic.org/resources/add_content/docs/95style/htm...
"If you disable exception checks and program execution results in a condition in which an exception would otherwise occur, the program execution is erroneous. The results are unpredictable"
It would be easy enough to also scan the source for sources of panics.
eval((lambda:0).__code__.replace(co_consts=()))- System.Diagnostics.Debug.Assert (on debug builds)
- Envinroment.FailFast (this is precisely what panic! is usually used for)
- void BlowUp() { BlowUp(); }
But, in reality, any unexpected Exception: how would you properly handle an unexpected NRE or index out of bounds? Either swallowing the exception (bad) or aborting.
That's not strictly true: https://doc.rust-lang.org/std/panic/fn.catch_unwind.html
Not exactly; “panic” is what Go calls exceptions.
Rust “panic!” macro, though, is more of a gentle abort (I guess it's like an uncatchable exception, but that qualifier so radically changes the nature that I can't see it as really the same kind of thing.)
What about zig, where there is no macros per-se, and you just write zig code which is then interpreted by the compiler...
I like compile-time programming, but I also recognize that it's programmer catnip: we love it because it lets us be clever, including in ways that aren't conducive to maintainability. That's a big part of why I like Crystal's macro system: I think it does a good job of balancing readability, expressivity, but also constraining the programmer away from compile-time gymnastics.
But like I said, I have no Zig experience, so it's very possible that Zig has much better ergonomics than I'm afraid of!
I don’t know what I’d propose instead that wouldn’t be just as bad, but I think it’s a wart.
Rust is also very focused on performance, and Rust users do care about making such trade-offs consciously. You care about binary size, I may care about inlined autovectorized code.
Everyone can agree with this. The question is whether this ought to be a property of the target platform or whether this ought to be a property of the software itself. Rust implicitly takes the stance that it is a property of the software.
Suppose we had a chip today that runs poorly due to icache thrash, so you rewrite portions of it with dynamic dispatch. Then someday we get a chip with an insanely large icache that would benefit from static dispatch. You rerewrite it. Or today if you work on embedded linux and want to use a rust library whose primary users are on powerful servers.
I will concede that I have no idea whether it’s possible to come up with a reasonable abstraction for this, because you certainly know more than me.
There are some languages that focus on "what" and magically optimize "how" (ISPC, Halide, and good'ol SQL), but for better or worse, Rust has chosen the "portable assembly" angle and micromanagement of everything. The upside is that you have very few surprises and "performance cliffs", and rest assured that if something is slow, you have enough control to fix it (as opposed to e.g. JIT-based language).
One of the strengths of Go and Java is the ease of writing tools. C++ on the other hand is a mess since it's hard to look through templates (concepts probably help) and macros (good luck).
https://eli.thegreenplace.net/2013/12/05/the-cost-of-dynamic...
I also wish they were more similar in both C++ and Rust. I find that most programs get more dynamic as they get bigger, get more usage, and get more extensible ... but the tendency is to want them to be as efficient as possible at the beginning.
Virtual vs non-virtual functions.
But I think what is more commonly thought of as "static dispatch" is various template programming patterns, as in the blog post, which are more flexible, and has equivalents in Rust AFAIK.
Overall it does feel like there are too many similar mechanisms that look extremely different!
Two ends of the spectrum and I think the ideal is somewhere in the middle.
Currently the following impls exist:
(1) `Vec<T>`, `HashMap<T>`
(2) `&[T]` (equiv to `slice.iter()`), `&HashMap<T>` (equiv to `map.iter()`)
(3) `&mut [T]` (equiv to `slice.iter_mut()`), `&mut HashMap<T>` (equiv to `map.iter_mut()`)
All I'm saying that `IntoIterator` should only be used for owned iterators, and it should be standard to provide `.iter()` and `.iter_mut()` for producing reference iterators. That is, only the (1) impls should exist, and the (2) and (3) impls should not. I don't understand how that requires it to act differently from every other method.
> • For producing “normal” borrowing iterators: &T for T in Container<T>
> • For producing iterators over mutable references: &mut T for T in Container<T>
> • For producing “consuming” (i.e., by-value) iterators: T for T in Container<T>
> • For producing “owned” (i.e., copying or cloning) iterators: T for T in Container<T: Clone>1
Life is too short for this kind of unreadable crap.
Implementation is the challenging part: the standard library and community ecosystem is full of legitimate uses of panic, and there are instances where panics might appear in the source code of dependencies but not in (optimized) builds.
Yes, but the whole point of `no_panic` is that in this context there are no legitimate uses of panic. If that means making a bunch of parallel APIs that return results for this super-correct code then so be it.
This would not protect against stack overflow or similar environmental panics.
However that doesn't mean Rust is the only answer, there are plenty of memory safe languages to chose from.
(And, for what it's worth, there are plenty of "cool jobs" writing C/C++. I have one!)
Firecracker could be equally well done in many languages. It happened that the people assigned it at the time wanted production Rust experience, and were senior enough to be allowed to, despite the predictable risk of staffing difficulty after some other language steals away Rust's hipster cred. (Which one will, in time, as sure as seasons turn.)
It is a common experience for systems written in the a fad language to end up being re-implemented in a language easier to hire for. E.g. I see remarks on HN about this being a principal driver of Erlang hiring, lately.
I expect Firecracker could be done about as well in Rust, C++, Haskell, Java, Ada, or Typescript; or even in Common Lisp, C, or Assembly language.
Every choice taken has consequences. Different choices have different consequences. Avoiding a choice has consequences.
Linux is (still) coded all in C. It is a terrible choice, but it works well enough to keep a billion phones and a billion websites operating. Language isn't fate.
But Firecracker is not a difficult project, so if somebody really wanted, they could do it in asm. Likewise, Common Lisp. Not many would choose that, but it could perform as well as the others listed before it. Some people would even choose C.
...and Dropbox can be replaced with a few shellscripts + SFTP.
Come on. Something like Firecracker has got to be both complex and extremely hard to get right. Complex because it has to deal with the ultimate in simultaneous state changes: a captive entire virtualized operating system. Hard to get right because it has a tremendous number of integration points with obscure and quirky external systems (KVM, Linux itself) at an extremely low level.
Nobody is claiming all bugs will go away. But the claim that new classes of bugs will manifest specifically to take the place of memory safety bugs is extraordinary and needs justification.
It's a bit like going into the wilderness and packing survival gear: the argument "you don't need a warm jacket, because if you get lost the starvation will kill you even if the frostbite doesn't" doesn't hold up.