Ok, so Rust. Why Rust? The problem I was facing was this: I simply miss writing code. As you might guess, being a software engineer is quite a different experience these days. At work I still review a lot of code, but I hardly write any code anymore. I needed a project for evenings and weekends where I get to do some coding. A good excuse to do that is to learn a new programming language.
One of the few major programming languages I don’t know yet is Rust. Not sure why I resisted so long. But I’ve finally given in, and I’m glad I did. It’s a really interesting language, especially when you come from C++ like I do.
The learning setup I came up with is this: I do a new project in Rust and let Claude mentor me, review my code, and provide scaffolding and hand-holding. The project itself is not that important but here is a very short pitch: a chat app using the iroh network layer. Claude and I agreed on a spec that we wrote together. The spec has milestones and for each milestone Claude sets up the files and a partial implementation and leaves some functions for me to implement. Claude also provides some unit tests and leaves me some unit tests to write. When I’m done with a milestone, Claude reviews it, gives me suggestions on how to do idiomatic Rust etc. And then we start the next milestone. We’re on milestone eight right now, already deep in tokio territory.
So far I’m loving it. It feels good to write code. I’m super slow but I don’t care. Rust made some interesting choices, especially around ownership. For a C++ head like me, a good analogy on how it feels to code in Rust is visiting a left-driving country and having to drive there for the first time. The defaults are all on the other side of the road: in C++ you have to explicitly move an lvalue; in Rust, assignment moves by default.
I’m not sure yet what exactly I will write about regarding Rust and this project on this blog. I guess I should have thought about that earlier. Maybe interesting code snippets that surprised me or where I had difficulties. In these early stages I’m still struggling with Result and Option and their pretty large API surface. Claude almost always finds ways to shorten my expressions into simpler, cleaner one-liners.
Here’s an example from a decoder for z-base-32, a human-friendly base-32 encoding. For each input character I need its 5-bit value, which is its position in the alphabet. Any character that isn’t in the alphabet is an error. This is what I wrote:
for (i, c) in text.chars().enumerate() {
let bval = u8::try_from(c);
if let Ok(b) = bval {
let pos = ALPHABET.iter().position(|&a| a == b);
if let Some(p) = pos {
acc = (acc << 5) | u16::try_from(p).unwrap();
// ...
} else {
return Err(DecodeError::InvalidChar { ch: c, index: i });
}
} else {
return Err(DecodeError::InvalidChar { ch: c, index: i });
}
}My version is very C++: check, unwrap, check again, and handle the same error twice. Claude did this:
let value = u8::try_from(c)
.ok() // Option<u8>
.and_then(|b| ALPHABET.iter().position(|&a| a == b)) // Option<usize>
.and_then(|p| u16::try_from(p).ok()) // Option<u16>
.ok_or(DecodeError::InvalidChar { ch: c, index: i })?; // u16, or return Err
Every step that can fail stays inside the Option, and only at the end does ok_or turn a None into the error and ? return it. One error path instead of two, and no nesting.
Anyways, to be continued.