The pain of learning Rust
I recently saw several posts from ESR discussing his attempts to learn Rust, for use in rewriting NTPsec: “Rust vs. Go” and “Rust severely disappoints me”, as well as “Rust and the limits of swarm design”. These posts gave me the incentive to write down some thoughts I have on the difficulty of approaching Rust. In pariticular, I have noticed are some commonalities among those who have initial difficulties with Rust’s ownership model.
(Of course, in this post I do not intend to address ESR’s comments on Rust’s crate environment or the necessity (or lack thereof) of being “batteries included”. And I certainly wish to avoid appearing to present “stupid flamage from zealots” or being a “clueless Rust fanboy” (Heaven forfend!).)
There are two kinds of people who will have excessive difficulty with Rust ownership:
Those who have extensive experience with C and to a lesser extent, C++.
Those who have not had any experience with un-managed memory.
(And yes, I’m aware that those two categories cover just about everyone.)
I myself am a member of the first group. I grew up with C. C is my native tongue. In particular, I have a lot of experience with C on “normal” systems—the workstations and personal computers in which C originated.
C, the language, has essentially no compiler-enforced rules preventing the programmer from doing hideously stupid things with memory. Feel the urge to pass a pointer to a pointer to a
struct ssl_ctx_st to a function expecting a pointer to a
struct ssl_ctx_st or to a function expecting a floating point number? Want to return a pointer to a buffer you allocated on the stack? Sure! Go ahead! The compiler will generate code that does exactly what you told it to do. It’s up to the programmer to figure out what went wrong and why, after the resulting nuclear apocalypse.
In order to be productive with this sort of entertainment, I have internalized a whole lot of rules describing the “C virtual machine” and what you can and cannot do with it. This, incidentally, is why I mentioned “normal” systems above; there are machines out there that violate what I have internalized as the “C virtual machine”, for which the code that I might expect to work happily will instead fail dramatically. That is the primary difficulty in writing “portable” C code: the rules for doing so are even stricter than the rules for 90% of the systems in existence, and code that works almost everywhere will blow up on that one ridiculous machine.
Having internalized those rules, I am in the position of instinctively writing code that I know to be (or quite reasonably expect to be) memory safe in this specific time and place, but I am unused to having any tools that try to verify that for me. I sit down and write me some Rust but when I try to compile it, the compiler becomes very unhappy. The compiler is unhappy because it cannot verify that what I have done is safe in general and I am unhappy because I know that what I have done is safe in this specific case.
The rub is that the compiler is more likely to be right than I am.
Anyway, at this point, I have to “fight the compiler”. I can either abandon the technique that I am trying to use and find something more to the compiler’s liking, or I can try to add enough information to allow the compiler to judge that my technique is safe. Unfortunately, the tools to do the latter are a bit obscure, seemingly weird, and often not available. After all, Rust is not a dependently typed language and adding a proof of correctness to my code would likely be somewhat more painful than using Rust as it exists.
As a result, to learn Rust I have to unlearn some of the lessons I have internalized from years of writing C and relearn the related, but different, approaches needed for Rust.
Those who know C++ are in a similar, but less extreme, position. Rust is, after all, aimed at C++. Programmers using “standard” C++, specifically avoiding the keywords
delete and avoiding raw pointers, are likely to have an easier time. On the other hand, there will be bumps in the road particularly regarding other parts of Rust.
The second group, above, are in a whole ’nother world of hurt: they’re engaged in a journey of adventure and discovery wherein they need to learn how to manage their own memory without amputating any of their own limbs. Fortunately, on the one hand, Rust inherits many of C++’s techniques such as RAII, which make the task significantly easier. And on the other hand, the compiler’s insistence on its ownership model should ensure that they don’t have to also learn to love valgrind. But still, managing memory is a new requirement that has nothing to do with the actual logic of the problem to be solved, and the compiler is going to be an enemy until the programmer has learned how to handle this extra requirement.