Rust Is (often) Not the Language for Me
I love Rust's emphasis on safety and correctness. I'm not referring to one specific feature like the borrow checker. I'm referring to what seems like pervasive part of the language and culture of Rust. From forced error checking, to exhaustive pattern matching, to validating Unicode character boundaries, everything is built with an eye towards up front in your face safety and correctness.
This emphasis creates a bit of friction and even temporary frustration when writing Rust code. My intuition is that this friction is usually worth it. I feel a sense of satisfaction as I work past it. But Rust has another source of friction that really makes me chafe. The source of this other friction is also a pervasive part of the language and culture of Rust. I'm talking about performance and zero cost abstractions.
Do I Care About Runtime Performance?
I like to think I care a lot. But I don't care about it in the same way or to the same degree most Rustaceans seem to. I don't think Rust makes the right trade offs for the types of programs that I write most often.
A good example is dynamic vs static dispatch. A dynamic dispatch function call requires an extra vtable lookup. It might make certain optimizations hard or impossible for the compiler to pull off. But usually, I don't care and I'd rather not think about it. Rust makes me to pretend to care in situations where I just don't.
Please Just Copy the Damn String
I've come across a lot of libraries that have a tiny bit of initialization in the form of a string. The strings might represent paths or URLs or passwords. In the interest of avoiding memory allocations at all costs, the library will often borrow the strings you give it instead of making a copy. This is where my suffering begins.
I'm now doomed to explicitly manage the lifetime of this object. If I want to abstract away some of the implementation details of the object with my own wrapper, I'm in for a bit of pain and failure. Why do I have to deal with this? I'm not instantiating this object very often.
Here I'm complaining about the third party library ecosystem and not so much
Rust itself. It makes sense if a function like String.trim()
returns a slice
instead of a whole new string. But for a templating or database library that
I'm instantiating just once for every HTTP request or once for the entire
lifetime of the application, the single string copy would have next to 0 impact
on performance.
Garbage Collection is Great
The borrow checker eliminates much of the need for a garbage collector, but that's not why I love the borrow checker. To me it's just an interesting coincidence that the borrow checker helps with memory management. I've usually been content to eat the run time costs of GC.
There have only been two significant times in my career where the cost of Garbage Collection really mattered. The first time was when I was building a game. Carefully controlling the number of allocations and deallocations and controlling how and when they happened had a meaningful impact on how much we could get done in a tick. The other time was when I wrote a kind of specialized columnar database in a garbage collected language. A year or so later when it had to deal with more data and more queries than ever, long unexpected GC pauses became a regular occurrence.
We solved the latter problem by throwing more memory at it. I wasn't thrilled with the solution, but it was maybe the right thing to do at the time. This is one of those rare instances where a full rewrite would have been completely justified. If Rust had existed as a stable language, maybe rewriting it in Rust would have been a good solution.
Sometimes I just want some big, twisty, circular data structures that will be traced out of existence when I don't need them anymore.
Async/Await is Awkward
I don't care about the size of my futures. I'm perfectly happy allocating each future on the heap. I just want to be able to stick async funcions in data structures, pass them as arguments, generically mix and match them, and use them in traits. These tasks vary from awkward to impossible.
Wrapping my head around async/await in Rust, was more challenging than I expected. It didn't help that this was one the first thing I tried to use in Rust. I suspect all of its complexity is a direct result of aiming for a zero cost abstraction.
What Is the Language for Me?
I'm not sure the language I'm looking for exists. This language would look a lot like Rust. If I were to create such a language, I'd call it R$. The R would be a respectful nod to Rust and the dollar sign would be a strong hint that its abstractions might cost you something. If this hypothetical language did exist, you can be sure I'd still have something to complain about.
This little essay is not really a criticism of Rust. It's still partly a love letter to the language. I love it so much I wish it felt like the right tool for me in every context. Rust is doing exactly what it was designed to do.