Great things about Rust that aren't just performance
Monday, January 6, 2025
Nearly every line of code I write for fun is in Rust. It's not because I need great performance, though that's a nice benefit. I write a lot of Rust because it's a joy to write code in. There is so much else to love about Rust beyond going fast without segfaults.
Here are a few of my favorite things about it. Note that these are not unique to Rust by any stretch! Other languages have similar combinations of features.
Expressive type safety
There are two aspects of Rust's type system that I really enjoy: type safety and expressiveness.
I got a taste of this expressiveness back when I learned Haskell, and had been seeking it. I found it in Rust. One of the other languages I use a fair amount at work1 is Go, and its type system is much harder for me to express ideas in. You can do it, but you're not getting the type system's help. Rust lets you put your design straight into types, with enums and structs and traits giving you a lot of room to maneuver.
All the while, it's also giving you good type safety! I can express a lot in Python, but I don't trust the code as much without robust tests. You don't have a compiler checking your work! It's remarkably helpful having Rust's compiler by your side, making sure that you're using types correctly and satisfying constraints on things. To call back to data races, the type system is one of the reasons we can prevent those! There are traits that tell you whether or not data is safe to send to another thread or to share with another thread. If your language doesn't have the equivalent of these traits, then you're probably relying on the programmer to ensure those properties!
That said, Rust's type system isn't an unmitigated good for me. It can take longer to get something up and running in Rust than in Python, for example, because of the rigidity of the type system: satisfy it or you don't run. And I find a lot of Rust that uses generics is very hard to read, feeling like it is a soup of traits. What we make generic is an implementation question, and a cultural question, so that isn't necessarily inherent to the language but does come strongly bundled to it.
It doesn't crash out as much
Okay, I have a beef with Go. They included Tony Hoare's "billion-dollar mistake": null pointers. Go gives you pointers, and they can be null2! This means that you can try to invoke methods on a null pointer, which can crash your program.
In contrast, Rust tries very very hard to make you never crash.
You can make null pointers, but you have to use unsafe
and if you do that, well, you're taking on the risk.
If you have something which is nullable, you'd use an Option
of it and then the type system will make sure you handle both cases.
The places you typically see crashes in Rust are when someone either intentionally panics, for an unrecoverable error, or when they unintentionally panic, if they use unwrap
on an Option
or Result
.
It's better to handle the other case explicitly.
Fortunately, you can configure the linter, clippy, to deny code that uses unwrap
(or expect
)!
If you add this to your Cargo.toml file, it will reject any code which uses unwrap
.
[lints.clippy]
unwrap_used = "deny"
Data race resistance
It's so hard to write concurrent code that works correctly. Data races are one of the biggest factors contributing to this. Rust's data race prevention is an incredible help for writing concurrent code.
Rust isn't immune to data races, but you have to work harder to make one happen. They're almost trivial to introduce in most languages, but in Rust, it's a lot harder! This happens because of the borrow checker, so it's harder to have multiple concurrent actors racing on the same data.
You get more control, when you want
With Rust, you know a lot more about what the CPU and memory will be doing than in many other languages. You can know this with C and C++, and newer systems programming languages like Zig. Rust is a little unique, to me, in being notably higher level than these languages while giving you ultimately the same amount of control (if you break glass enough, for some things).
You're still subject to the operating system, most of the time, so you can't control the CPU and memory fully, but you get a lot more control than in Python. Or even than Go, another language used when you need good performance.
This lets you predict what your code is going to do. You're not going to have surprise pauses for the garbage collector, and you're not going to have the runtime scheduler put some tasks off for a while. Instead, you know (or can determine) when memory will be deallocated. And you ultimately control when threads take tasks (though with async and the Tokio runtime, this gets much muddier and you do lose some of this control).
This predictability is really nice. It's useful in production, but it's also just really pleasant and comforting.
Mixing functional and imperative
Rust lets you write in a functional programming style, and it also lets you write in an imperative programming style. Most idiomatic code tends toward functional style, but there's a lot of code that uses imperative style effectively as well!
This is pretty unique in my experience, and I really like it. It means that I, the programmer, can pick the paradigm that best fits the problem at hand at any given moment. Code can be more expressive and clearer for the author and the team working on the code.
One of the cool things here, too, is that the two paradigms effectively translate between each other! If you use iterators in Rust, they convert into the same compiled binary as the imperative code. You often don't lose any efficiency from using either approach, and you're truly free to express yourself!
Helpful compiler errors
A few other languages are renowned for their error message quality—Elm comes to mind. Rust is a standout here, as well.
Earlier in my career, I was abused by C++. Besides all the production crashes and the associated stress of that, the compiler errors for it were absolutely inscrutable. If you messed up a template, you'd sometimes get thousands of lines of errors—from one missing semicolon. And those wouldn't tell you what the error was, but rather, what came after the mistake.
In contrast, Rust's compiler error messages are usually pretty good at telling you exactly what the error is. They even provide suggestions for how to fix it, and where to read more about the error. Sometimes you get into a funny loop with these, where following the compiler's suggestions will lead to a loop of suggesting you change it another way and never get it to succeed, but that's fine. The fact that they're often useful is remarkable!
It's fun!
This is the big one for me. It's very subjective! I really like Rust for all the reasons listed above, and many more I've forgotten.
There are certainly painful times, and learning to love the borrow checker is a process (but one made faster if you've been abused by C++ before). But on balance, using Rust has been great.
It's fun having the ability to go ripping fast even when you don't need to. It's lovely having a type system that lets you express yourself (even if it lets you express yourself too much sometimes). The tooling is a joy.
All around, there's so much to love about Rust. The performance and safety are great, but they're the tip of the iceberg, and it's a language worth considering even when you don't need top performance.
Go was introduced at work because I advocated for it! I'd probably use Go for fun sometimes if I were not getting enough of it at my day job. It's a remarkably useful language, just not my favorite type system.
They're expressed as nil
in Go, which is the zero value, and is the equivalent of null
elsewhere.
If this post was enjoyable or useful for you, please share it! If you have comments, questions, or feedback, you can email my personal email. To get new posts and support my work, subscribe to the newsletter. There is also an RSS feed.
Want to become a better programmer?
Join the Recurse Center!
Want to hire great programmers?
Hire via Recurse Center!