Initial thoughts on Rust

The first stable version of Rust has been released earlier this month and for the last couple of weeks the language was surrounded with even more hype than usually. Since people have started recommending it as a ready and complete replacement for C++ and others have responded with hostility, I decided to see for myself.

This post is a collection of loosely-connected notes and not a proper introduction to the language itself. If you’re interested in learning Rust the official Rust book is a good resource (and one that I have used).

Functional programming

Right off the bat, the inspirations from Haskell are quite clear and, at least for me, it’s tempting to program in a more functional style. While possible, it might not always be the best idea. Like in C++, each lambda expression is of its own, dedicated, type. As Rust has no garbage collection and defaults to stack allocation this is a necessity since closures can obviously have different sizes depending on what exactly is captured.

Nevertheless, Rust handles this quite elegantly with the use of the Fn trait. Writing functions that take other functions as arguments is easy and can be done efficiently, although the decision has to be made each time between a generic function parameterized by the argument type and a function that takes a reference to an Fn trait object. I suspect that the first option is more likely to inline a lambda expression used as an argument. Storing arbitrary function objects in data structures requires explicit boxing.

Overall, I found it easier to design Rust programs with a “C++-like” mental model than a “Haskell-like” one. While it might be possible to use some of the common Haskell tricks with Rust traits and generics, it’s probably better not to.

Error handling

The way Rust handles error conditions seems to raise the most controversy. The preferred way seems to be to use the wrapper type Result, which basically corresponds to Haskell’s Either. The API provides some useful methods like map() or map_err() which make working with Result a lot more convenient. Instead of Haskell’s special monad notation, one is supposed to use the try! macro. While this made me cringe at first, I got used to it after a while and I no longer feel that it’s morally wrong to use macros instead of some special syntactic sugar notation built into the language.

However, all of the usual objections to the Haskell’s Error monad still apply. The error-enabled results are contagious: Once you realize that one function might possibly fail you can find yourself redesigning half of your public API and refactoring lots of code to use the try! macro.

Thus, it’s tempting to use the other error handling mechanism—panics. They work by stack unwinding, just like exceptions, but their use is discouraged and you can only catch them at a thread level. Spawning a thread just for the sake of error handling is even less elegant than using macros and it’s not something I would do in library code—especially since panic handling is optional and not guaranteed to always work . It seems like panics are meant to be used mostly for non-recoverable errors.

Type system and lifetimes

A common complaint seems to be that programming in Rust involves lots of struggling with the borrow checker. Maybe I didn’t write enough code to run into problems or maybe I unnecessarily copy stuff too much but I never had that impression—all the common memory allocation patterns I would typically use in C++ work nicely in Rust.

Lifetimes are clearly not magic and not a replacement for garbage collection and you certainly cannot just forget about memory management. If you have data structures keeping external references, you’re stuck with generic lifetime parameters forever—including all other types that embed your structure. Or at least I haven’t figured out yet how to do it more elegantly.

What I liked the most about the type system is that move semantics makes a lot more sense than it does in C++. All types are non-copyable by default and passing a non-copyable variable as an argument to a function just forbids you from using it after the call—as if the variable went out of scope at that point. This is much more intuitive and makes things like std::move unnecessary.

Human factors

Quite a few languages have been developed over the years with the explicit goal of replacing C and C++. In order for Rust to stick around, it needs more than just to get all the technical parts right.

Rust might support a freestanding environment without heap or panic handling but I think most C programmers will consider it to be too complicated to use in OS kernels or hardcore embedded systems. Even C can be too high-level for these applications leading to unusual situations where a technically correct compiler optimization can still enable security bugs. All the C programmers who have been resisting C++ for the last 30 years will probably eschew Rust for similar reasons.

As for C++, it’s doing a decent job of replacing itself. Programming in it since C++11 is a refreshingly different experience and current proposals—like the one concerning the module system —might move C++ even closer to Rust. Rust has the advantage of being a “clean slate” approach but C++ can roll out new features gradually and already has huge adoption.

What, in my opinion, makes Rust quite different from the previous attempts is that from the very beginnings of the language there have been at least one big and serious project using it—Servo, Mozilla’s research web browser engine. It looks this provided Rust developers with frequent reality checks and made the language more pragmatic. If Servo replaces Gecko as the rendering engine of Firefox (and so far its performance looks very promising) the language will definitely stay around and other people might gain more confidence to use it in production.