Frustrated? It's not you, it's Rust
fasterthanli.me
fasterthanli.me
I've been rewriting a project in Rust. The original is a 20+ years C project (not created by me). Rewriting it in Rust has been a delight. So many surprises, discoveries, learnings.
I've first converted the codebase using C2Rust (surprisingly efficient). The code compiles and runs correctly. While Rust has been nagging me nonstop when I try to do things the original C way, and I still have a lot of weird flags on the code, once I rewrite some piece of code in idiomatic Rust, I end up with a much better design than before. Gone are weird linked lists, crazy custom hashmaps, strings regurgitated in memory, "buffers" holding any manner of data anywhere in memory. Instead, the new code is more efficient, more understandable, and of course safer - even if more restrictive!
I've found at least two major hidden bugs on the original codebase doing so too.
Rust certainly has a learning curve. It's not C/C++ nor Java/C#/JavaScript. But I'm more and more convinced it's the right way to do things in what's currently the C/C++ space.
In this sense you could have switched from C to almost anything else and have gotten similar benefits. Finding bugs and improving the design is typical for refactoring and redesign projects.
The way I see it, much of Rust, and this project specifically, is not about moving to a higher abstraction... It's about moving to the right abstraction.
Yes, I could have switched to just about anything else, and end up with an even higher level code. But that would defeat the purpose of keeping it small, fast, and memory conscious.
Rust is NOT a Stack Overflow snippet language. Attempting to do so will get you into trouble.
Rust does NOT like pointer magic very much. If you try to be too clever with pointers, you will fight with Rust.
That having been said, I really don't fight with Rust all that much. I don't understand everyone complaining about the borrow checker, but, then, I've been doing embedded C for a very long time now. So, a lot of my bad habits have been ground out of me--quite often very painfully--by chasing subtle bugs in production devices.
After a couple of those experiences, you begin to write very different C code.
I also found that people tried to solve lifetime problems with the wrong tools. Normally everybody reaches for & and mut and * in Rust but very few people pointed out that an extra set of {} in the right place was more likely to solve or illuminate the problem.
That is a great line!
Bad news - I just realized that I’m a stack overflow snippet programmer.
It’s been one of the least frustrating most satisfying programming experiences of my life. I don’t think there’s a better language to learn while learning how to write an emulator.
Combined with the plugin for vscode, it’s my wingman that points me in the right direction whenever I want to do something stupid. The compiler errors are amazingly descriptive for a newbie.
The limitations it puts on borrowing And sharing references forced my emulator to take on a design I hadn’t planned. And completely unaware, months later, I’m uncovering so many reasons why I’m so glad I was pressed to do it that way.
What do you understand by design in this case?
I wanted a design where every subsystem (ppu, apu, serial, controller, etc.) owned a reference to the singular MMU struct. This is how I’d do it in python. Makes referencing mmu easier.
Rust said no, you can’t have many mutable references. I looked around and found refcell and such. Ways to have shared mutability as long as I enforced certain lifetimes and other guarantees that I’m not going to cause memory problems by deleting what other systems expect to be there.
This was looking complicated and messy. Sure I could do it. But it spoke to me: “this is probably not right”.
I ended up with a design where I have a core “step” function that calls each system’s step function, in order, and passes in a mutable reference to MMU to each. Basically it loans out the MMU one at a time.
The result is that testing is so much easier because all my systems are stateless and my MMU holds all the state. I just assemble an MMU state the way I want for each test and evaluate how the system mutated it.
It also meant that implementing save/restore state was incredibly easy because my entire guest machine state was in one struct and all the systems were stateless so they didn’t need any logic to recover state.
You're probably not. That's kind of the thing. Rather, it's not that you're not smarter, it's just that you're not used to doing things properly, and Rust forces you to do things properly.
"Leave me alone, I know what I'm doing" says beginner juggling chainsaws on a unicycle.
Ultimately it comes down to this: when you don't write down all the information a compiler needs to generate an artifact, you're relying on hidden, un-stated assumptions. Such assumptions are subject to change over time. Relying on them isn't smart, it's dumb. It makes you feel smart. Just check the CVE list.
He's very positive and encouraging, which shows how much dedication he puts on his teachings.
Relax
This attitude continuously drives me away from learning Rust, didn’t find such attitude in Haskell or Lisp community which really took computer science and art of programming forward unlike Rust, which is similar to other competing niche languages.
Rust is one of few if only languages I am aware of that hit hard on data safety without a gc. That'll make it quite different to the programmer.
I'm too lazy to go find a reference right now but it shouldn't be overly hard.
> it's just that you're not used to doing things properly, and Rust forces you to do things properly.
Many patterns that rustc refuses aren't inherently wrong. They are because 1) you have to aid lifetime inference and memory management unlike GC languages 2) All safe patterns can't be verified by compiler, especially if threads are involved.
The cult like mentality of rust spammers on hackernews is making many people sceptic about it. Rust is actually well designed language for what it does. But stop pretending rust is God.
It's not about the patterns being wrong, it's about the patterns being verifiable and having a compiler that's on your side, not on the side of watching the world burn. The difference between C's laissez-faire approach, where no new warnings are added and anything's whatever you want it to be vs. a subset of provably correct things you can opt out of at your discretion.
Yes, it's not going to catch all edge cases, yes it's not perfect, however the idea people are behind is that it represents a fundamental change from "I'm sure the smart engineer knows what they're doing" to "hold on now, let's assume they don't, or will forget in 6 months, or some new person is going to have zero context." That model maps much better to the reality we have vs. the one we wish we had.
My opinion is hardly culty, I write ObjC at my day job at $BIGCO, I've done plenty of Swift, I've written plenty of embedded C, plenty of C++, implemented a ton of stuff in VHDL. Rust is my go-to for personal projects. It's genuinely better. Not perfect, better. And better's what I'm looking for.
Using Rust seems like it would make sense for an extremely low-level performance-sensitive application, but do people use it for higher level things? If they do, why?
On the Rust-written projects that I work on, I rarely have significant compiler errors that impede on my efficiency in adding new features.
After a while, you know that you're mutably borrowing something, or that you're crossing a thread boundary and are carrying something not thread-safe along.
Maybe like other low-level languages there's a large learning cost, but it gets better after a while.
When writing code that isn't that performance-sensitive, cloning is often an option (and gets you past a lot of grief with lifetimes), so is reference counting, etc.
Rust's set of rules just means you can never, for example, accidentally hold a reference to a value that's been freed - it'll error out, and then you can pick whether you meant to borrow it, to share ownership with reference counting, or to clone it.
Personally, I remember an interview of Keith Haring just before he passed away; he wasn't sleeping much to put as much of his ideas out as quickly as he could, he was about to die. He said something like he'd rather spend the time creating then perfecting his art. I don't know exactly what his words were but what he said made a lot of sense to me.
The popularity of C++ keeps growing for some reason and Swift offers a great dev experience!
On top of that, the layoffs in the Servo team is quite telling about how profitable the work they've put was.
Personally, I've found that the borrow checker for the most part doesn't get in the way. But part of the reason for that is because I've used Rust enough to know what the borrow checker expects, so I would just design things in a way that get accepted in the first place without having to think about it.
Someone new to the language won't have that prior knowledge, and will be more likely to run into the borrow checker and get frustrated because of it.
So with that in mind, I don't really find the cognitive overhead to be high when writing Rust. I also find that I like having the control over what can mutate what, and knowing that information by looking at the function signatures or variable definition. Also, Rust has all of the features I liked in Haskell when I tried it, while putting them into a package that is more familiar and comfortable to me.
Rust will catch mistakes you would've let through, but it'll only agree to verify a subset of all valid programs.
Yeah, but not all programs genuinely need to be multithreaded [0] that too with memory sharing, and even in a program very few data is shared. And a lot of patterns can't be verified by compiler to be amenable for static memory management. That's a tradeoff.
I am not telling rust's cognitive overhead makes it unsuitable for everything. It definitely has a place.
But unlike what a lot of people on HN tend to say, rust is not the perfect language for everything.
Languages like OCaml deserved a significant portion of hype that rust gets now. But there is no mozilla and no HackerNews spamming taskforce behind OCaml. On the positive side, all 5 companies using OCaml pay well ;)
[1] As a user, I don't want your code to exhaust all my cores, unless it is a batch job. I'd rather urge to choose better algorithms and data structures for the job and many times parallelism is just unnecessary.
For something where one has the freedom to pick anything they want, I'd take advantage and use something more user-friendly and with even less restrictions like Python or Swift. Although for heavy lifting I may still reach for C++ because of the massive ecosystem.
Languages like Rust or Ada with their heavy reliance on specific "annotations" to ensure safety are designed for professional teams that must fulfill certain quality attributes.
Furthermore there's always a tension between speed and safety in C and C++ programs which make them particularly prone to these errors.
In fact there are probably many people who are competent enough to write safe C++ code (C's a lot harder), but they code in environments where this is unlikely to happen.