Reading the Rust book
20 October 2022

I just finished reading the Rust book. It is not my first time through it, nor my second for that matter. It is however the first time I could see that Rust is significantly simpler than C++. Not as simple as C but something that it is possible to reason about without too much trouble. It is not hitting Haskell level of hey I know lambda calculus so I can reason about the evaluation and the type system using the same part of my brain. Well at least Rust doesn't hit that level for me.

Rather than talk about Rust I would like to first focus on chapter 12 where they get to develop mini-grep. A small toy application that's a good example.

In it you write these two bits of code

let mut results = Vec::new();

for line in contents.lines() {
    if line.contains(query) {
        results.push(line);
    }
}

results

and

let query = query.to_lowercase();
let mut results = Vec::new();

for line in contents.lines() {
    if line.to_lowercase().contains(&query) {
        results.push(line);
    }
}

results

They search for lines of a file that contain your query text and append them to a vector. One is case sensitive and the other it not.

I assume fairly readable to all.

The following chapter, after introducing iterators, rewrites the case sensitive example to look like this

contents
    .lines()
    .filter(|line| line.contains(query))
    .collect()

That's an improvement. Assuming you like that style which many do. I admit to being partial to it even though I understand the trade offs. Then the worry flag appears, "feel free to do this to the case insensitive one".

This is going to be ugly as they haven't done it in the book. It is important to note that the lines being returned should be in the original case. Here is my attempt.

let query = query.to_lowercase();

contents
    .lines()
    .map(|line| (line.to_lowercase(), line))
    .filter(|(low, _)| low.contains(&query))
    .map(|p| p.1)
    .collect()

It does exactly the same as the for loop but you really need to look at it to understand it. No where near as clean as the case sensitive version. Perhaps it is my inexperience with the style but I think I prefer the for loop in this case. In release they are probably equivalent but my brain struggles to be sure.

So if this was production code at work then I am faced with weird dilemma. I want to use the nice iterator version for the case sensitive version and the for loop for the case insensitive.

The trouble is they sit together in a file. People pattern match and they get a mental jolt from the two different styles. Why are they using different styles is a question bound to be asked. The lure to refactor would be strong and an engineer could waste their time arriving at the same solution I have and discarding it.

Sure I could leave a comment and even leave my iterator version commented out. Still people spend time trying to improve it. The alternative is I use the iterator method and people spend cycles figuring out if what I have done is correct. Either way brain cycles are spend.

Of course perhaps I have missed a trick that simplifies it or maybe my brain is not quite trained enough in idiomatic iterator code.


The second "Huh" occurred in the last chapter where they develop a small multi-threaded https server. The fact they do this in 50-ish lines of code is a testament to Rust and its' std library. Again a great example.

The final part is about cleaning up after yourself using the Drop trait. RAII for C++ devs. They want to move a thread out of a Worker object that is, itself contained in a vector. Once they have ownership they can call join on thread to end it.

So we convert

struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}

to

struct Worker {
    id: usize,
    thread: Option<thread::JoinHandle<()>>,
}

This is the code that drops it.

for worker in &mut self.workers {
    println!("Shutting down worker {}", worker.id);

    if let Some(thread) = worker.thread.take() {
        thread.join().unwrap();
    }
}

I only every like using optional types to model whether an object should exist or not. Making a member variable optional just so you can take over ownership feel wrong. I almost instantly arrived at the alternative

while let Some(worker) = self.workers.pop() {
    println!("Shutting down worker {}", worker.id);

    worker.thread.join().unwrap();
}

I was much happier with as I avoided the optional, although I am not experienced enough in Rust to say if it truly is a good idea. The irony is the next section used the same optional idiom on another member variable that I could not think of an alternative for so perhaps it is an idiom I should just get used to.


In summary, I breezed through the book in a few evenings. Helped by the fact I had read it before. I was thinking enough I started to question some of the code, probably wrongly but questioning is good for learning. Rust is good and a nice improvement on C++.

The book is very good introduction, read it you might like it.

I feel like I need to spend more time with rust macros and just write more code in it. As as structured learning goes I am not sure if there is a follow on intermediate book or whether I should jump on the Unsafe rust book they have produced. My instinct is I should probably just start writing code in it. A lot of code :)