Rusty Gripes

Has Rust achieved its goals of high performance, high reliability and high productivity?

Rusty Gripes

At the end of 2019 I used the fabulous Advent of Code as a learn-by-doing outlet while getting acquainted with Rust. As a long time embedded C coder who missed the Ada boat, Rust seemed like a great opportunity to explore modern programming language features, while still remaining applicable to systems programming. My professional practice with C has benefited immensely by also adding Python to the toolbox, by studying algorithms by way of C++, and by becoming handy with specific-purpose languages like Perl, Octave and Bash.

Rust is purported to be high performance, high reliability and high productivity. Sounds like something a professional C programmer ought to be familiar with. My verdict... eh. It's a little strong on the pedantic and a little weak on the pragmatic. Here's what got me offside:

  • consts are variables with borrowing rules and lifetimes, enums are difficult to treat as numbers, and there is no #define equivalent. So magic numbers rule. Sheesh.
  • The documentation on if let swap the lvalue and rvalue as if no big deal. The keyword is explained as a shorter match but in that case why wouldn’t you just use if? As Rust By Example shows, if let is actually a way of destructuring and doing comparisons for simple types that don’t support equality operators. Which in itself is another uphill battle for something so fundamental.
  • for loops can’t skip forward n iterations.
  • So much explicit casting, even for safe, well defined and "obvious" casts.
  • So many braces.
  • enums require lots of work and aren’t very useful. Half a dozen traits need to be added to act like a normal enumeration. No support for max/min. Can't have duplicate values. Hardly worth it.
  • trees. What a mess.
  • Ternary statements are verbose, despite their primary purpose being terseness! Meanwhile the ? keyword is used esoterically.
  • String and &str are confusing. For something so fundamental, it's pretty cruel.
  • Variables can be &mut, but Vec’s have to be mut &mut ?
  • Variable naming is checked by the compiler! So many exceptions to machine-enforced rules. But you can't reason with a compiler...
  • You use flatten() sometimes, and flat_map(|i| i.iter()) other times. Good luck.
  • Arrays larger than 32 elements are hard to work with. Seriously, beyond 32 elements, it's tough going.
  • All variables must be initialised, which for complex variables can be hard to do, and often is meaningless.
  • What's the lifetime of Vec<&str> as struct members? Oh my, tricky question!
  • std HashMap has poor performance on integer hashes, and relies on you providing a source of randomness. Again, fundamental feature, mountain to climb.
  • Arrays have very limited applicability, but Vec has worse functionality. For example, adding elements to the front ain't pretty.
  • Type annotations on, eg., sum - what a dog’s breakfast.
  • No local static variables in safe code. Either write neat, well contained code, or be safe. Your choice.
  • Holy “cannot borrow mutable because moved immutable” Batman. I've never met a compiler so restrictive and unhelpful.

Rust reminds me of a static analysis tool, turned up to "extremely grumpy", that cannot be disabled. Yes, technically there's a mathematically provable flaw in this line of code. But let me worry about provability later, once I know what I want to prove. So much of coding is exploratory, so it's exhausting having to fight the compiler because it has detected that technically, if a million holes in the swiss cheese line up (which is impossible in practice but mathematically possible) what you've written is not perfect, and that's all it's going to tell you. We learnt all this from the Ada era - provability theorems are the holy grail of safe code, but they are so tremendously laborious that it is simply not practical to apply them every where.

Besides, if you're determined enough to ensure that you don't "borrow mutable" after "moved immutable" by some obscure-but-technically-true route, then you have the capacity to write safe code. Given the opportunity, you will already write high reliability code. Enforcing it in every line of code you write (90% of which you will delete before the work is done), is counter-productive. Haven't we learnt that premature optimisation is the root of all evil?

Meanwhile, so much attention has been giving to "productivity" in the wings (dependency management and the like), that core features are awfully immature. There's plenty of ways to get dependency management over the line, but if we don't have a straight-forward way to add another element to our 32 element array, then we've got more immediate problems.

Rust is a fine example of the trap of programmers everywhere:

"A mind all logic is like a knife all blade. It makes the hand bleed that uses it." –  Rabindranath Tagore

Yes, it's very clever how you get the computer to doomsday scenario every line of code you write, but is it useful? Programming languages are tools - to meet their purpose they must enable the programmer, not simply admonish them.