I've been writing everything in rust these days - even replacing my python scripts. When I started, I spent most of my time wrestling with the compiler, but as I've grown more comfortable with the language I've been spending more time thinking about the problems I'm trying to solve.
The tooling in rust is great. Cargo is wonderful, dependency management is sane, I write more unit tests, and the documentation is fantastic. Rust ships with the API reference and offline versions of a dozen books. You can access these docs with:
rustup docs
And you can view an offline html render of most libraries by cloning the source (or just finding it in the local cargo registry ~/.cargo/registry/) and then running:
cargo doc --open
Refactoring in rust is amazing. Once you have something locked in with types and unit tests you can make a change and the compiler will help you find and fix every reference. I recently replaced chrono and crono-tz with jiff in a library I'm working on. This dependency was deeply connected to every other module, but rust refused to compile until I'd caught every reference and when I was done, it worked on the first try.
When writing in rust it's common for programs to work as soon as you've satisfied the compiler.
Despite my proficiency I'm still sometimes confused about the syntax. I recently ran into this:
match exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY) {
Some(orientation) => match orientation.value.get_uint(0) {
Some(v @ 1..=8) => println!("{} Orientation {}", filepathstr, v),
_ => eprintln!("Orientation value is broken"),
},
None => eprintln!("Orientation tag is missing"),
}
I had to look up the '@' operator in this line:
Some(v @ 1..=8) => println!("{} Orientation {}", filepathstr, v),
Apparently '@' provides pattern binding and '1..=8' is right inclusive range literal. Basically this match arm matches on an Option containing a value within a range of 1-8, and captures the match in 'v' so we can print it. The following '_' arm will match everything else, including Some(9), and None.
I found the pattern matching rules confusing at first. The idea of capturing and unwrapping a value from the left-hand side of a declaration is strange.
For example:
if let Some(color) = favorite_color {
println!("Using your favorite color, {color}, as the background");
} else {
println!("Using blue as the background color");
}
I've gotten more comfortable with this syntax, but I still have think about it.
The deref trait is amazing. It makes working with pointers painless. I'm always amazed how easy it is to pass things around by reference.
The type system is powerful, but I'm still building a full war chest of examples to refer to. Capturing behavior in the type system is a good way make the compiler work for you and is one of the main reasons people are excited to rewrite system utilities in the new language. The problem is the complexity this brings.
The kernel guys are wrestling with rust.. The seasoned c programmers don't want to have to learn and maintain a new language, but the rust guys are finding bugs and bad abstractions. in their code.
It's important to bring in new blood, but I can understand the reluctance for kernel maintainers to accept rust when their c code seems to be running fine and has been running for decades.
The language difficulty has a filtering effect among developers. I'm generally happy to replace system utilities with ones written in rust, and I'm more likely to view a project favorably if it's written in rust because the only developers that can write it are skilled by definition. As the language gains wider adoption the quality might come down, but rust has found a sweet spot.
Despite the complexity and compiler constantly telling me I've done something wrong, programming in rust is fun. I haven't felt this excited about a language since python.