Rust from a Gopher - Lesson 15..20 - The Final Fork

Hello and welcome to the ninth and final post in my series about learning Rust. In case you want to hit it from the start, here’s a link to the first one! This entire series covered my journey from being a completely land-locked Gopher to becoming (hopefully) a hardened Rustacean, able to skitter the hazardous seabed of any application safely.


This post covers chapters 15 through 20 of The Book. I decided to compile my thoughts on all six of the final chapters in a single post because to be frank, I am becoming less shocked by the lessons I’m learning and subsequently have a whole lot less commentary to make.

Overall the journey of picking up the basics of Rust has been fantastic. It’s been the first language I’ve made a concerted effort to try to learn since Go, about 7 years ago. You can’t understand how excited I am to go and get burned in the real world now. So let me begin summarizing what interested me through the final legs of the book.

15. Smart Pointers

Deref & Drop

Note to self; Smart Pointers (SPs) implement Drop and Deref. Okay. Drop == out of scope action. Deref == letting instances of your SPs act as if they were refs. Good start.

Basic deref makes sense, but it feels like I’ll have a learning curve here. Go is very gentle with automatic dereferences for functions and values. Not sure Rust will be the same.

Box

After running through the Cons example, it seems like Box is just a way to say ptr, as a type? Is it possible at all that a Box ptr can end up pointing to something in the stack?

Qualifier update on this note having read unsafe Rust: Is it possible to do this without unsafe Rust

Implicit Deref Coercions with Functions and Methods

Deref coercion happens automatically when we pass a reference to a particular type’s value as an argument to a function or method that doesn’t match the parameter type in the function or method definition.

Holy word soup batman, this was a lot for me to parse. It does seem very convenient though.

Also, dropping via std::mem::drop is super good to know about.

Rc, The Reference Counted Smart Pointer

We use the Rc type when we want to allocate some data on the heap for multiple parts of our program to read and we can’t determine at compile time which part will finish using the data last.

Good simple explanation.

Ownership still makes me scratch my head though as it makes me think of read/write capabilities. But here ownership means “What is pointing to a value”. It makes sense because as long as anything is pointing to that value, then the value cannot be removed, so the value really is being owned by everything that points to it. For some reason moving from single owner to multiple owner has really made me rethink ownership.

Rc is a really cool datastructure. I actually want to do some leetcode graph examples and use Rcs to cheat out “degree” lists for all nodes in a graph.

RefCell and the Interior Mutability Pattern

This sounds juicy!

The advantage of checking the borrowing rules at runtime instead is that certain memory-safe scenarios are then allowed, whereas they are disallowed by the compile-time checks. Static analysis, like the Rust compiler, is inherently conservative. Some properties of code are impossible to detect by analyzing the code: the most famous example is the Halting Problem, which is beyond the scope of this book but is an interesting topic to research.

Wasn’t expecting to see a reference to the halting problem in this chapter. I think there are probably better examples to use for logical issues a compiler can’t catch though. I don’t see how the halting problem is analogous to RefCell/unsafe code. The halting program is strictly for turing machines, compilers can do simple infinite loop detection and also, computers are not strict turing machines, which means we can apply other heuristics to determine answers to halting problem examples.

Anyway, RefCells - good to know about, but this stage I almost feel like the concepts are in one ear and slowly leaking out the other!

Ref Cycles - Weak Ref

God, I’m starting to get dizzy by the end of this chapter. I had nearly forgotten the differences between a Box and a Rc by this stage! God only knows what wild code using smart pointers looks like; just making a simple node datastructure in the examples became quite convoluted, quite quickly… And there I was, thinking Rust would be a great language to leetcode in, hah!

On a more serious note, I am actually scratching my head as to how Weak Rcs work. If the Rc is truly deleted from the heap when it is dropped with no strong refs to it. How do future upgrades on the Rc “know” to return a None Option? Doesn’t this sound like we would need a runtime to track this state? Or is it that the value inside the Rc is dropped, but the Rc Smart Pointer itself doesn’t get dropped until the weak refs hit zero?

16. Fearless Concurrency

Yay, concurrency! This is a concept I at least use, regularly.

Rust needs to have nearly no runtime and cannot compromise on being able to call into C to maintain performance

I don’t understand what this means. Is this referring to using something like pthreads for concurrency?

One strangeness I found from the Book was that it says thread::spawn takes a closure. I tested it with a function though, and it worked just fine - but it had to have no args. A closure would seem to make sense for basically all cases I think.

Move it!

Wow, so you just put move in front of your thread closure and rust figures out what exactly it needs to move ownership for, based on the closure refs! That is neat. Also, thread spawning and collecting is really simple, coming from the ease of Goroutines, I love that.

Here’s the idea in a slogan from the Go language documentation: “Do not communicate by sharing memory; instead, share memory by communicating.”

I’ve never heard this “slogan” before - and it shows what I know. But, really the only time I’ve seen shared memory used in Go was for IPC with C++ applications. Within a single Go application it has indeed always been channel based message passing.

The Flow of Channels

The Book describes channel with a waterway analogy. I’ve never actually thought of channels like that. I always envisioned a radio channel. Send/Receive over a certain frequency? I think the water way is better probably. It’s simpler to think about what a buffer is in terms of a waterway, compared to a buffer inside a radio.

The Sending Multiple Values and Seeing the Receiver Waiting example is really, really cool. Why? We get to rely on the sender getting dropped to signal the receiver that the channel is closed and subsequently the iterator also ends.

For some reason I didn’t think the for loop channel behavior was supported in Go, but it certainly is. In practice, I have never needed to use them though, as I’ll typically not want to only block while waiting on a message. There’s almost always something else to do when there are no messages in the channel. In Rust, it looks like you would match over rx.try_recv() within a loop to do non-blocking alternatives whilst waiting.

I’m not sure if you can make unbuffered channels in Rust though, as it seems the default has a limitless buffer? I made a toy program dump many messages to a channel in a loop, but I eventually killed it when the memory usage was reported at over 20 GB. I guess you probably need to implement your own wrappers (or find a crate) to make use of unbuffered or limited buffer channels. That is a little sad.

Mutexes

Definitely see the similarities of Mutex and RefCell. Overall Mutex looks very simple to use, and obviously the automatic unlock through the Drop Trait is really sweet; no more defer m.Unlock() everywhere like in Go…

17. Object Oriented Programming Features of Rust

What a strange chapter to have in a book, so late as well. I’m intrigued about what decisions brought this placement about. Recently I caught a NeXT marketing video on youtube. I was surprised by two things - the first was a very, very young looking Avie Tevanian (his Oral History session with The Computer History Museum is a good watch), and the second surprise was how hard they were pushing NeXTSTEP as an Object Oriented focused OS. I guess that was from around ‘89? So I don’t know. To me OO is a fairly dated approach, I’m guessing this chapter is paying respect to how ubiquitous the paradigm became though.


Gimme da Objects
  • Objects, check.
  • Encapsulation, check.
  • Inheritance… ??

It’s interesting that the authors view default method Trait implementations as a substitute for the code reuse aspect of inheritance. In Go, it’s a little simpler, you can just embed a struct or interface into a struct, acting very similarly to OO inheritance.

I liked that the book covered static vs dynamic dispatch for Trait bounds vs Trait objects. I was lucky enough that @ssokolow already took time to teach me those details in the comments of my lesson 10 post, huge shoutout to ssokolow!

State Pattern Example

While it was neat to see Traits being used as “State objects” in the example, my main takeaway was simply not to rely on old fashion OO patterns and instead do what makes sense with your language.

18. Patterns and Matching

The first “oh that’s cool” moment with patterns was seeing compiler warnings for irrefutable patterns when used with an if let. It makes sure your developers are aware of crappy code, or missed refactorings or the like.

The example trick question bamboozled me big time in this chapter:

fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        Some(y) => println!("Matched, y = {:?}", y),
        _ => println!("Default case, x = {:?}", x),
    }

    println!("at the end: x = {:?}, y = {:?}", x, y);
}
// Prints:
// Matched, y = 5
// at the end: x = Some(5), y = 10

I couldn’t wrap my head around why the Some(y) would bind to the x, in the match. Only when I went back to the match chapter did it make sense. I had completely forgotten that match statements can bind variables to the struct/tuple that’s being matched against. I can’t believe how quickly I forgot how awesome matches are.

Hide your Vars!

Hide unused var warnings by prefixing with a ‘_’. This doesn’t sound bad at all. Your IDE can refactor that quickly - I’d really like this feature in Go.


Other bits I found great from this chapter were Struct field shorthands and using _ for unused function parameters. Oh, and also match guards appear powerful. In general, match just looks phenomenal.

19. Advanced Features

This chapter discussed many features, including the baddest of them all…. unsafe

Nullified

[raw pointers]… are allowed to be null

Where did null come from??? I can’t believe The Book just dropped a null on us and didn’t even bat an eye. Did it truly mean null or are we just talking about a zero value?

Safe Unsafe

I found the slice example was brilliant for demonstrating where unsafe code makes sense. It’s a (classic) case where the developer knows better than the compiler. You use unsafe to rearrange the wires under the hood in one quick minute, but in reality it is completely safe code.

Globality

Up until this chapter I did not know Rust supported global variables. Given the catch is that to use them as mutable variables requires unsafe, I can understand why The Book didn’t expound them earlier.

Safety Questions

The Book’s unsafe Trait explanation didn’t really contain any examples that I could attach to. I’m scratching my head a little to think of what an unsafe Trait may look like in the wild. Hopefully I can find something soon to put this to rest.


Using unsafe to take one of the five actions (superpowers) just discussed isn’t wrong or even frowned upon.

I’d like to know if this is true. In my extremely limited experience with the Rust community I’ve already seen “but it uses unsafe code” as a qualifier for a library. I sort of have an inkling that people have been burned by unsafe libraries before, perhaps leading to such disclaimers being the norm?

More on Types

I found the default type parameters in Traits to be fascinating. Combined with associated types, Traits look to become super powerful. I dig this!

Type aliases are also good, I love using them in Go to make nice APIs in a lightweight manner. The Book examples are good too, as I never considered using them for something neat like combining traits and heap allocation:

    type Thunk = Box<dyn Fn() + Send + 'static>;

To elevate type aliases even higher in Rust, you can use them on types with generics too, doing neat things like making concrete only some of the generics in a type:

    type Result<T> = std::result::Result<T, std::io::Error>;
Macros

Honestly, most of the macros section of this chapter went over (or through) my head. Once I hit the DeriveInput snippet my eyes glazed over. This is probably a topic I’ll revisit later in my Rust life, once I’m more experienced with regular code and also just using a variety of existing macros.

In Go, I’ve used the bundled code generation tools a couple of times, but that is a lot simpler. The Book also mentioned that many aspects of writing macros are possibly subject to fewer restrictions in the future too; which makes it sound like there are some old kinks in Rust that are being worked out to make macro writing slightly better.

20. Build a Webserver

Chapter 19 was the final “learning” chapter of the book. 20 was an example of building a multi-threaded webserver in Rust basically only using standard library features. I skimmed through it as it was largely a rehash of learned concepts. I have in my plans a pit stop to check out tower-rs at some stage, although I don’t plan to do any backend networking projects in Rust for quite some time.

The End

This is it!!!!! I’m FREE to go forth and CODE in Rust now. Liberation!!!! Yes!

I’m so thrilled that I made it through the book. Overall it taught me many, many lessons and for a completely free resource, has been fantastic. It’s definitely my goto recommendation for newcomers to the language to check out. I have been informed that Programming Rust is also a fantastic learning and reference Book on the topic too - though I am all read out right now. When the second version comes out later in 2021, I may pick up a physical copy of it.

At this point, I am fully ready to start the next phase of my master plan, which I will write about shortly. Happy holidays everyone!

P.S. / Spoiler: I’m currently knee-deep in Bevy as I post this; I can’t wait to include hacked up Rust games in my next blog post!

comments powered by Disqus