Love-hate relationship with Rust language (part 2)
About half a year ago, I wrote an article about my experience with Rust. It’s interesting to reread it now, as I’ve learned a lot and moved from the “complete beginner” to the “intermediate” level.
Rust was kicking my ass back then pretty much all the time. Today, it feels like we have a tie most of the time. I know how to do a bunch of things, and it does it for me. However, often enough, it reminds me who the boss is and forces me to spend a lot of time-fighting some Rust idiosyncrasy.
So, let’s go back to my normal format: Rust (Good, Bad, and Ugly) from an intermediate Rust engineer perspective.
Good
I can repeat here almost word-for-word what I wrote last time. The greatest thing is how safe it is. If it builds and passes the tests, I can be sure that there won’t be crazy memory corruption, race conditions, and so on.
It just works! (like Steve Jobs used to say).
Another thing that I gradually started to appreciate is that it forces you to think and reason about the memory. (I do understand that it’s not a benefit for the majority of the apps. However, for apps that require a small footprint and high performance, thinking about memory should not be an afterthought, and Rust helps with this.)
Bad
Both prototyping and refactoring in Rust are painful. When writing the original article, I didn’t know whether the problem with refactoring and prototyping was me or the Rust language. Now, I can say for sure it’s the language. Two things about Rust make it complicated:
- Rust is a language of purposefully leaky abstractions.
Just to give you an example. If you have a reference to some object and keep it on any structure, it will cause this structure to have a lifetime specified, and if this structure is stored in another struct, that one will require a lifetime specified too. So, having a reference way down in the guts will bubble up to the top of your structure hierarchy.
2Rustaceans: Yeah… Yeah. I know you can do Rc or friends. I was trying to show that it leaks implementation details.
- Rust requires you to satisfy _ALL_ requirements until you can build it.
This sounds strange. All compiled languages force you to satisfy all requirements to be able to build the app. So, what is the difference?
Let me give you an example in C/C++. You can write things dirty (don’t free memory, don’t have the perfect protection against race conditions). You can throw something together to check that your basic premise works and, after, clean it up.
This is impossible in Rust. You are either 0% done (nothing builds) or 100% done (everything works and has no undefined behavior). Nothing in the between. Mic drop.
BTW. I was talking about it in “Ugly” section in the previous article, but it looks like the experience got downgraded to “Bad.”
These peculiarities make it hard to prototype and do refactoring. Sometimes you want to check whether you are moving in the right direction. And you don’t know whether your approach is feasible in Rust until you have solved everything and satisfied all constraints.
Time and time again, I start small refactoring, Rust throws a bunch of compilation errors, I fix them, and it through other errors, and so on, until I run into something on another side of the project that prevents me from doing this whole thing altogether. This leaves a very unpleasant taste in my mouth.
BTW. I call this whole thing (with more painful prototyping and refactoring) “Rust tax.” Unfortunately, I have to pay it each day. However, with time this tax gets smaller.
Somebody told me that Rust is a startup killer (because of these problems). I sympathize with people dealing with Rust tax, but I strongly disagree with the sentiment. There is WAY-WAY-WAY more to building a startup than just slapping several prototypes together.
Ugly
Rust compiler is a marvel. I have never seen such a detailed explanation of errors and messages to help you resolve them.
However, sometimes it gets messy, and when it does… Good luck figuring out what the hell is wrong. On top of that, the compiler could be wrong sometimes (maybe wrong is an overstatement, rather I should say that compiler, in some situations, is not smart enough to figure something out).
I stumbled on a problem recently when the compiler couldn’t figure out what I wanted, giving me an error. I finally found a workaround that I couldn’t understand, and people on the forum walked me through it so that I could grok it. (BTW. After about three pages of explanations, I still have only a vague understanding of the underlying cause).
It doesn’t happen often, but considering that making Rust happy is not straightforward, you can freak out when you stumble on something like that, since you will think it’s your code doing something wrong (vs compiler).
BTW. I spent on C/C++ (on and mostly off) about two decades, and during that time, I stumbled upon one case when the compiler was (borderline) wrong. It took a year to get there with Rust. Actually, it could have been less than a year. There were a number of times when I just didn’t have enough knowledge to prove that I was doing the right thing, and the compiler was off.
P.S. And getting back to our original dilemma to build our app in Go or Rust (which I posed in the first part of this article). I still ponder routinely whether the drop in personal productivity was worth building our software in Rust (vs. Go). Building it in Rust let us never deal with the garbage collector (which was critical for our product). However, as I said, we are paying Rust tax for this each day.