Last week I wrote my first Rust program!
I've been excited about Rust for a long time, and after finally taking the plunge, I'd like to share some of my thoughts.
Before diving in, let's briefly review what Rust is all about:
- Systems programming language (think: modern replacement for C or C++)
- Zero-cost abstractions for common programming paradigms
- Functional programming
And, its biggest selling feature of all:
- Compiler-guaranteed memory safety (no data races, dangling pointers, or other similar memory issues
This makes Rust particularly useful for concurrent/multi-threaded programming, since its compiler prevents many bugs that tend to appear in these circumstances.
A lot has been written about how Rust has enabled developers to parallelize their code relatively painlessly.
But my simple Rust program does not need concurrency. This post is about a Rust port that does not involve parallelization.1
My program is a simple command-line time-tracker. It reads and writes to an array of log entries stored in
~/.log.json. (Eventually I want to write a Josh Avenier-inspired front-end for viewing the log, but for now logging itself is enough.)
log program needs to do the following things:
- Store the start and end time for working on projects, as well as significant events within those projects
- Output the current in-progress project
Yes, that's all.
This is something that could probably be solved with a
jq-powered shell script, but I wrote the first version using Node.js.
Surprisingly, it was extremely slow. Reading and decoding a nearly-empty JSON file was a serious bottleneck, to the point where even outputting the current in-progress project had a noticeable delay on my laptop (about 1.5-2 seconds). This delay is probably the warm-up time for Node rather than any issue with JSON parsing.
When the goal of your program is to log productivity, it's a little contradictory to have an inefficient logger. The logger should stay out of your way so don't have to spend time waiting for the program to run, possibly losing focus or forcing a context-switch along the way.
Okay, so the Node version wasn't cutting it. But why use Rust? Why not a language I already know, like Crystal, Lua, Io, or Go?
Well, I knew I wanted to start learning Rust at some point, and this seemed like a good starting point - simple enough to be a good learning tool but complex enough to be challenging. In other words: for a language beginner,
log is an achievable challenge.
How'd it go?
The newly Rust-ified version runs significantly faster with no noticeable delay. Mission success!
Some other positive notes:
- Once I got used to it, I came to really appreciate Rust's functional-programming match syntax.
- Rust's compiler errors are helpful and often suggest the correct code change.
- I also think Rust has struck a better balance than Go, in terms of compiler errors. Rust will issue (override-able) warnings for unused variables, imports, etc. but it will not stop compilation. While it's good to have a strict compiler, which Rust and Go do, in my opinion Go's compiler is too strict.
- crates.io and cargo (Rust's built-in package manager) are fantastic! I believe they will be the driving force behind Rust's continued popularity and growth, even more so than memory safety.
- json-rust uses Rust's macros to great effect, making it extremely easy to encode JSON data inline.
- In contrast with the usual Rust experience, I didn't have to fight the borrow checker much. Instead, I fought the type checker a lot. (Once again, the compiler was friendly and helpful about suggesting workarounds.)
- This is partially because I was learning a new language at the time, but this port has made it clear to me that Rust has a significantly higher up-front cost than other languages. You pay this cost in exchange for future safety, speed, and stability, but it is a cost regardless. It took me noticeably longer to get a working prototype in Rust than in Node, ignoring the time it took to sort out newbie syntax and language errors.
- Similarly, the Rust version of
- Sometimes Rust's strict compiler forces you to write some seemingly-unnecessarily-long code, like
env::args().nth(2).unwrap(). These examples could be refactored slightly but I still don't love even short method chaining for converting a JSON string to a Rust
Stringor accessing a command line argument.
Overall, I enjoyed learning a bit of Rust. I was able to learn enough to write this (relatively simple) program in one sitting, and even added some trivial features along the way! The syntax took some light adjustment on my part, but I've come to like it. I legitimately had fun writing Rust! I'm sure some of this enjoyment is simply out of novelty, but on the other hand I can't think of a time where I've enjoyed the act of writing C.
Furthermore, Rust forced me to think a little harder about my code, which also helped me uncover a data storage bug, where the Node version would allow you to log events even if they had no project to go to. This bug fix might have more to do with the fact that I rewrote the entire codebase, and less to do with Rust itself.
I think, for now, I will still turn to Crystal if I need to quickly write well-performing code, but Rust has made it onto my radar. I hope to eventually move from Crystal to Rust but until I get used to it, writing in Rust will be slower and more painful than writing in Crystal for me. If I have the time and energy, like I did for
log, I might use Rust so I can learn it slowly over time.
And if I ever need to write concurrent code, I'll let Rust do its magic.