Justine's Corner
Thoughts about Rust
My introduction to the language
Approximately 6 months ago, I started seeing Rust everywhere in my newsfeed. The language seemed to have interesting features, and most importantly, a good entrypoint for newcomers : the Rust book. I am fairly proficient in Python, and always am willing to go lower-level to understand things better. So I thought Rust could be a good language to add to my arsenal, as a complement to the higher-level Python. In the past, I dabbled a bit in Go but did not adhere to it; I also had experience in C from my teenage years, and a bit of C# as well.
So, I started reading the Rust book, taking notes.
My journey through the Rust book was a rather pleasing one, albeit a bit tedious at times. People always suggest reading it. But it’s not a novel; I had to write things down and try all the concepts in the book to really learn things. And boy, does this language have a lot of features ! And a lot of syntax to go with it. Lots and lots of things to learn. Great, I thought. With all these features (and tools), this is gonna be a really useful language. Let’s have a look at the chapters of the book:
- Cargo basics
- Common programming concepts
- Ownership
- Structs
- Enums
- Pattern matching
- Packages, Crates, modules, privacy
- Collections (Vecs, Strings, etc)
- Error Handling
- Generics, Lifetimes
- Tests
- Iterators and closures
- More Cargo
- Smart pointers
- Concurrency
- Pattern matching
- Unsafe Rust
- Advanced stuff
Please keep in mind that this book is intended for beginners to learn the basics. The least you could say is that this a solid primer : that’s a lot of new things to learn. Some are harder to learn than others, ownership especially. But, everything is well detailed. The Rust book is a brilliant introduction to a language.
So, I read (almost) all of that, took notes, tried stuff. There’s a lot of concepts I did not really grasp, and I must admit I skipped advanced features and unsafe Rust (keeping them for later). Proud of going so far, I started writing code. I worked on a bunch of pet projects : an ls clone, playing around with the little web server shown in the book, a clone of Jinja, etc. Rust is great for such little projects.
Afterwards, I wanted something bigger, so I started writing my own shell : Sqish. I learned a lot doing so, and ended up with a mostly functionning and fast shell, with integrated keyboard macros.
Lesson learned : Error handling
Rust is really hard to write at first. Ownership often comes to bite you in the ass, but you learn your way around it eventually. My Rust code tends to be really lengthy. One of the mains tenets of the language is safety : memory safety (think ownership) and error handling. Error handling is everywhere in Rust, and every time you get a result from a function, it always comes wrapped in a Result<Value, Error>. I appreciate the base concept. I like the idea of writing code that always shows a useful error to its user.
But another tenet of Rust is strong typing : no “stringy-typed” stuff, everything is typed correctly and cannot really be converted. Cool, uh ? More safety. Ho, and also, in Rust, everything comes in crates. The language has fairly reduced standard library; things such as time handling, random number generation, etc… come in separated crates. Crates full of functions (cool) and structs (mmm) made to do different things. Including the results ! Functions will often return a result such as Result<ValueYouWant, ErrorStructYouNeverSeen>. Why ? Rust has a some nice syntactic sugar for handling results :
fn my_function() -> Result<String, String> {
let a = some_other_function()?;
return Ok("Hello there".to_string());
}
Here, my_function returns a result containing a String (either a result, or an error). the ? after the call to some_other_function means that if it returns an error, my_other_function will directly return said error and not go any further. Cool ! But both of these functions need to use the same type… Which rarely happens, especially if you use multiple crates. You will have to read and understand quite a lot of docs.
So you need to resort to other things. How do you handle errors ? There are many ways. Matching :
//I have a result called res
let val = match res {
Ok(v) => v,
Err(e) => {
//Do stuff about that error
//And maybe give a default value to val
};
Matching is the most “sane” way to do things. It’s also lengthy and I hate its syntax, with its (values) that are always named after letters to keep things short. Really, you should match only when you intend to do something else with the error than printing it.
If you just want to show the error, use expect :
let hello = hello_world().expect("Could not say hello");
Shorter, hey ? If hello_world returns an Ok(val), it goes to hello; otherwise, the program stops and prints the text given to expect. Cool. Though I’m not sure how I’m supposed to show what went wrong. That’s not a great error I have there : it tells nothing about why I could not say hello.
There probably is a better solution I’m not aware of.
Ho, you can also use unwrap(). You’re supposed to use it to extract the value inside a Result when you’re 100% sure it will be Ok(). It’s super short:
let a = bake_cake().unwrap();
If somehow bake_cake() send an Err(), the program will crash and tell us what is in the Err() it found. So… It seems like a really short and convenient way to print errors ?
In my own projects, all my errors are Strings and I put ?’s everywhere. And for now, I’m happy with it; though I can see the appeal of having an error contained in a well-designed struct you use throughout your library.
Why is everything so complicated ?
I always try write clean, short, readable code. As much as possible. I rarely succeed to do this in Rust. The fact that everything always comes packed in a Result is a source of length. It really came to me when I was writing Sqish. I wanted to use as few crates as possible, except for termion (I’m not that mad). Well, it made me realize one thing, as I was going mad over the lengthy code I had to write : Rust is not made be simple ! Or rather, it’s not made to be short. It’s not a scripting language. Everything must be cut in small parts, and assembled afterwards.
So, trying to keep things concise, you sometimes end with things looking like :
let filename = filepath
.iter()
.last()
.unwrap()
.to_str()
.unwrap();
But that’s not the worst. I have tendency to use too many loops and blocks. In Python, the indentation system keeps thing easy to read. Rust uses curly braces for everything, which means my code ends up looking like a fishing contest :
fn list_dirs(dir: &String) -> Result<Vec<String>, String> {
let paths = match fs::read_dir(dir) {
Ok(c) => c,
Err(e) => {
return Err(format!("Got error {:?}", e))
},
};
let mut ret_vec = Vec::new();
for path in paths {
match path {
Ok(ref e) => {
if e.file_type().unwrap().is_dir() {
&ret_vec.push(format!("{}", path.unwrap().path().display()));
}
},
Err(_) => (),
};
}
return Ok(ret_vec);
}
It just means I need to write better code and be more patient. I’m not a professional developer, mind you : I’m a systems administrator. We are used to short, simple scripting languages, that allows us to quickly spin solutions and bodge our way out of issues.
So yeah, I have room to grow on that front.
Why is the compiler so nice ?
I haven’t used a ton of compilers, but man, is the Rust one nice. The errors are useful and well-explained 99% of the time, everything is nicely coloured, there’s a ton of useful things. When you get an error, you get a short explanation and are given a command to get the fully explained version in your terminal. I have no complaints here. I just wanted to state what a pleasure this compiler is. My biggest pleasure when writing my shell was shooting down every warning given by the compiler until it came down to 0.
To wrap things up
I started writing this because I was a bit frustrated with Rust. I was trying to use the “time” crate, and pretty much could not do what I wanted. The thing has too many structs to my liking, and simple operations can be complex (why is converting a datetime to a Unix timestamp so darn complicated ?!). But looking back, I like Rust. But, you have to keep in mind what it is : a language made to optimize things to their finest, not to spin out services on the go in 50 lines (use Python for that, or maybe Go I’m unsure). So, it can be really frustrating sometimes. The time between having an idea and implementing can be looooooooooong. Which can be frustrating. But, the result often is really solid, and safe.
Also, Ferris is cute.