Wirth's compiler
I have a small 1,900 formatted line web service, that along with its 20 dependencies, takes cargo
42 seconds to complete a fresh debug build, and 23 seconds to build after modifying one file. On a computer running a Comet Lake i7, with DDR4 memory, and an 870 SSD, it takes cargo
1 second to compile and link 82 lines of code.
It is true that rustc
necessarily has to do more work than most compilers, because of the language’s complexity. That would make a good explanation for why the compiler can only build 10,000 lines per second instead of 100,000 lines per second. It is not a good explanation for why it compiles more than 1,000 times slower than a C compiler.
The fact that anyone thinks this is even close to acceptable is perhaps a symptom of a greater problem facing software today, but even with this pervasive complacency, few languages compile slower than Rust. The absolute state of the compiler even has some users gaslighting themselves in to believing that it’s, actually not so bad.
One might say that rustc
is not Rust. This is true, but also moot. rustc
is not just the reference implementation, it’s the only complete implementation, the only choice for the vast majority of Rust users.
Saving the best for never
Rust’s premier feature, the borrow checker, is touted as the secret to code that’s fast, and safe. When a new user encounters pain with the borrow checker, they’re usually told they will eventually learn to write their code in a borrow checker friendly manner.
But alas, this is only true for the most trivial problems. What actually happens in Rust codebases, is that people learn how to work around the borrow checker. Don’t want to prove your mutable reference isn’t aliased? No problem, just use RefCell
. Don’t want to worry about the lifetime? No problem, just use Rc
.
In the end, much of the static safety promised by the borrow checker is moved to runtime, all while still paying the ergonomic cost of static safety.
Error handling with a touch of programming
Each standard module has its own unique, incompatible error type, so whenever an error has to be passed to the caller, it has to first be explicitly converted. The de-facto way to handle this problem in applications is to use anyhow
, a crate that adds a base trait for new error types to use, and macros to wrangle it.
Zig provides a similar mechanism, but baked in to the language instead. Zig’s design is not without its downsides, but it is significantly more ergonomic, and isn’t implemented with macros.
Uninventing concurrency
Idiomatic concurrency in Rust is cooperative rather than preemptive. For a function to be run concurrently it must be decorated as async
, something known as coloring. The de-facto concurrency runtime, tokio
, doesn’t allow you to do anything inside an async
function that might block the scheduler, such as CPU intensive tasks. For that, tokio
recommends rayon
, an entirely different concurrency system.
Plenty of languages that came before Rust demonstrated the correct way to do concurrency, with Go probably being the most famous. But in fact, Erlang solved this problem 30 years before Rust decided to screw the pooch.
Rewrite it in Rust
The most widely shared criticism of Rust has to actually be its community. This isn’t without good cause, but I also don’t think it’s a good reason to avoid the language. The community is actually quite helpful, even if terribly conceited.