One day when doomscrolling on Twitter, I saw this tweet from James Coglan and decided it would be a good topic for a long form writeup.
For completeness here’s the full tweet:
gonna keep thinking about how rust pulled off something that shouldn’t work on paper: making devs “do more work” with more static modelling and a very hard-to-please compiler, and ended up with anyone going “this is actually great”
While I am sure James’ tweet is at least partially sarcastic, it did get me thinking about why people do put up with Rust’s compiler. This post is a long form version of my thoughts in response to James’ tweet.
Let’s talk about the concept of a “compiler”. Throughout this post I’ll use the term loosely and sometimes incorrectly to mean “interpreter” too. I will talk about a compiler accepting your program, which strictly speaking interpreters, like python, almost always do. However they might immediately crash with runtime errors. A python program that runs without syntax errors is considered “accepted”, for compiled languages a programs is accepted if the compiler compiles it successfully.
So why does anyone put up with the Rust compiler? My short answer is: the level of input effort required to please the Rust compiler pays off in the output program. To make this clearer let’s define “input effort” and “compiler output”.
- Input effort means the time and mental energy required to make the compiler accept your program.
- Compiler output means the likelihood that the resulting program works correctly, is safe, and performant. The “If it Compiles it Works”-factor if you will.
With Rust specifically the input effort is quite high. The compiler is strict about a lot of things and will not accept programs that don’t follow certain rules. For example, the Rust compiler has to be able to prove that references to values follow the rules of ownership; i.e. no immutable and mutable references to a value can exists at the same time. However, by enforcing these rules the compiler output can have certain guarantees. For example, programs accepted by the Rust compiler are mostly free from race conditions because of these ownership rules. I put up with the Rust compiler because when my program is accepted by the compiler, I have high confidence that it will be correct. Said another way, Rust has a high “If it Compiles it Works”-factor.
The Compiler Pain Index
With this concept of input effort and how it translates to compiler output we can plot different languages on a graph which gives us the compiler pain index. The index is the ratio of input effort to compiler output. None of this is particularly scientific, but it doesn’t have to be to get the idea across.
A language like Python is different from Rust. The input effort required to please the compiler is very low, the code needs to be valid Python syntax, but in return we get very little in terms of compiler output. There are few guarantees about the resulting program and my confidence that it will work is much lower than with Rust. Python has a low “If it Compiles it Works”-factor and that’s okay because with Python the level of input effort feels appropriate for the compiler output. A bad language would be one that required unjustifiably high input effort for the resulting compiler output. By comparison, a good language would be one where the input effort is low compared to the resulting compiler output.
Every programmer will have different ideas about which languages are “good” and “bad” using this metric. Personally, I feel like Java requires too much input effort to justify the compiler output it provides.
Of course, this is just a single metric to measure a programming language by and, as I’ve said, programming is about tradeoffs. Java might require too much input effort for the compiler output, but it has significant other upsides which often makes it a good choice.