Rust Part II: What is it good for?
Last time I talked about Rust I mentioned that I wanted to like the language but I couldn't find a good reason for using it. Luckily for me, the last Advent of Code gave me the perfect reason for doubling down on Rust, and here's my updated report.
In case you never heard of it, Advent of Code is an online competition that takes place every year in December. It is structured as an Advent calendar where you get a puzzle every day, and where the puzzles get harder and harder every day. Plenty of people use this competition as the perfect excuse for learning a new language, which is how I ended up programming lots of Rust in my spare time.
So here they are: in no particular order, these are the things I like, dislike, and feel mildly uncomfortable about Rust.
Things I like
The one thing I like the most about Rust is the power of the match
operator
combined with enums
. Unlike in Python, where the implementation of the match
statement is pretty dangerous,
Rust makes it easy to program the type of code that's easy to write, read, and
maintain:
let mut pos = 0;
let mut depth = 0;
for instruction in orders {
match instruction {
Instruction::Forward(meters) => pos += meters,
Instruction::Up(meters) => depth -= meters,
Instruction::Down(meters) => depth += meters,
}
}
Then, there are the compiler errors. While not true for external crates (we'll get to it), compiler errors in Rust are generally helpful, identify the actual source of the problem, and sometimes even give you good suggestions on how to solve the issue. Gone are the days in which a compiler error meant "I know an error happened 50 lines above, but I'll complain about it here instead".
And finally, as someone who has been doing mostly Python for the last years, it feels so good not to have to worry about indentation anymore. This doesn't mean that I'll stop indenting my code - instead, it means that I can finally move a function around without worrying about pasting it one indentation to the left and turning a class into a class and multiple pieces of code that don't compile.
Things I hate
I am puzzled by how aggressively unhelpful arrays are. The puzzle for day 25 could be easily solved (spoilers!) by writing
horizontal_row = horizontal_row>>1 && !(horizontal_row || vertical_row)
but I ended up having to implement it with Vectors of booleans instead. Why?
Because I didn't know how many bits horizontal_row
would have at compile time,
and Rust refuses to create arrays with dynamic size.
I am sure there is a way to keep a large binary in memory and manipulate it at
the bit level - otherwise, you wouldn't be able to use Rust for serious game development.
But whatever the method is, it is well hidden.
And on the topic of that puzzle, I come back to one of my main complaints
from last time: popularity is not the correct way to decide which library is
the best one for the job. Do you know the difference between the bitvec
,
bit-vec
, and bitvector
libraries? Can you add either of them to your code
without worrying about the developer going
rogue?
How about the fact that the first result
that comes up for rust bit vector
is an accepted StackOverflow answer
suggesting bit-vec
... which is no longer maintained?
Minor annoyances
I still can't make sense of the module system. I mean, sure, I know how to put
functionality in sub-directories, but that doesn't really explain why I would
choose between lib.rs
, day24.rs
, or day24/vm/mod.rs
. The
book
could use some improvements on this topic.
If I'm doing something like u16 = u16 + u8
(or even better, u16 += u8
),
the compiler should cast the last value automatically.
u8 += u16
? Sure, I get it, that's an overflow waiting to happen.
But there is no need for me to get in there and write u16 = u16 + u8 as u16
when we all
know the data fits just fine.
The collect
function is very finicky. This is a function that I used quite
often in constructions like .map(|x| something(x)).collect.to_vec()
, but more
often than not it will complain about not knowing the type required for collect
even though there is only one type that would make sense.
And since we are talking about the compiler, one of the crates I needed (it was
either nalgebra
or ndarray
, where I suffered the same problems I had
with bit vectors) had a nasty side effect: if one of your instructions
failed to compile, they all stopped compiling. Good luck finding the one
line that needs fixing!
And finally, I ran a couple times into functionality that had been deprecated in favor of functionality that doesn't currently exist. Not cool.
Conclusion
Would I use Rust again? Yes.
Is it my most loved language? No. But under the right circumstances I could see it happening.
What is it good for? Last time I jokingly said "writing Rust compilers", and I wasn't that far: it's the right programming language for apps that need performance and memory safety, and where we are willing to spend some time calculating who is borrowing from whom in order to get code with fewer bugs. So it's pretty much C++, only with borrowing replacing memory allocations.
I like the idea of giving my original project another try, but I can't make any promises. The Advent of Code has already pushed forward the date of my next project by a couple months, and the time it's taking me to migrate my infrastructure to Ansible is making everything worse.