Stream: bookclub

Topic: Programming Rust


view this post on Zulip Matthew Sanabria (Oct 08 2024 at 04:20):

I'm reading the Programming Rust book: https://www.oreilly.com/library/view/programming-rust-2nd/9781492052586/

I'll post some of my chapter notes here tomorrow and as I go. I'm on chapter 16.

view this post on Zulip Don MacKinnon (Oct 08 2024 at 21:24):

Matthew Sanabria said:

I'm reading the Programming Rust book: https://www.oreilly.com/library/view/programming-rust-2nd/9781492052586/

I'll post some of my chapter notes here tomorrow and as I go. I'm on chapter 16.

Did you read "the book" before this one?

view this post on Zulip Matthew Sanabria (Oct 09 2024 at 01:47):

Don MacKinnon said:

Matthew Sanabria said:

I'm reading the Programming Rust book: https://www.oreilly.com/library/view/programming-rust-2nd/9781492052586/

I'll post some of my chapter notes here tomorrow and as I go. I'm on chapter 16.

Did you read "the book" before this one?

I did read the official book before this one but I was really busy during that time so I didn't study it as much as I wanted to. This time around I've been studying and doing some projects to solidify the learning.

view this post on Zulip Don MacKinnon (Oct 09 2024 at 14:49):

Nice, would be interested to hear your thoughts on it once you finish!

view this post on Zulip Matthew Sanabria (Oct 10 2024 at 03:05):

If you're on a time crunch to read this book then you can probably skip chapters 1 and 2 since they just introduce the why of Rust and a tour of its usage. Chapter 3 is where the primary Rust types are introduced. The real content starts in chapter 4 where the book discusses ownership and moves.

Chapter 4 really clarified ownership and moves for me by linking Rust types to the stack and the heap. For example, String types are on the heap and number types (e.g., i32) stay on the stack. You later find out that this is powered by Box types and Copy and Clone traits. As functions are called values are moved into the functions unless they are passed by reference (i.e., borrowing) this moving of values is what throws people for a loop at first but the book explains it well with comparisons to Python's referencing counting and C's manual memory management.

view this post on Zulip Matthew Sanabria (Oct 10 2024 at 03:08):

Chapter 5 laid out the rules for references. Basically, you can only have ONE of the following.

This chapter also discussed lifetimes, describing them as a contract around how long data can live with respect to other data. Really helpful when reading function signatures to understand how they will use the parameters they accept. If you omit lifetimes the compiler will infer them for you.

view this post on Zulip Matthew Sanabria (Oct 10 2024 at 03:08):

I'm on Chapter 16 now, but I'll continue posting my notes from previous chapters over the next few days.

view this post on Zulip Matthew Sanabria (Oct 18 2024 at 04:41):

Chapter 5 covered references in depth, showing how Rust eliminates entire classes of bugs by their rules around references. The rules are mutually exclusive.

The chapter then covered how to use references in functions, both as parameters and returns. References as a parameter do not move the underlying variable into the function which is great when you want to perform some operations with that variable but don't want to own it but not great when you want to mutate that variable and return some mutation. For that there's a concept of interior mutability which is deferred for a later chapter.

Lifetimes were discussed in depth and the many examples cleared up my confusion around lifetimes. Basically lifetimes allow you to see the behavior (contract) of how long data should live with respect to other data. Most useful when reading function signatures. Even if you omit lifetimes, the compiler with infer them.

view this post on Zulip Matthew Sanabria (Oct 18 2024 at 04:42):

Chapter 6 covered all the expressions in Rust and that's pretty much everything that's in Rust because Rust is an expression-oriented language. Things like match, if let, closures, etc. There's not really much to report here because it's mainly around the syntax of Rust but there are cool things compared to other languages.

For example, you can assign to variables from a match.

let res = match work() {
  "foo" => "fooval",
  "bar" => "baval",
  _ => "defaultval",
}

The break keyword can return values too.

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {result}");
}

Outside of that it's pretty much as you'd expect from other languages.

view this post on Zulip Dustin (Oct 18 2024 at 15:52):

Your example is missing one of my favorite implications of that! You could have counter mutable _only_ inside the expression or only scoped inside the expression. This means you can control the mutability even within a single function on a variable.

view this post on Zulip Dustin (Oct 18 2024 at 15:53):

I think one of the easiest examples of this is initializing a Vec but having it no longer be mutable after the first initialization

view this post on Zulip Matthew Sanabria (Oct 18 2024 at 17:50):

100%. That's a huge strength of the Rust syntax.

view this post on Zulip Matthew Sanabria (Oct 18 2024 at 17:51):

I'll admit that I'm copying and pasting these notes from when I read those chapters. I'm on chapter 17 now but I've been spacing out the posting of my old notes so that I don't just dump them all here at once.

view this post on Zulip Matthew Sanabria (Nov 08 2024 at 04:24):

Chapter 6 covered all the expressions in Rust and that's pretty much everything that's in Rust because Rust is an expression-oriented language. Things like match, if let, closures, etc. There's not really much to report here because it's mainly around the syntax of Rust but there are cool things compared to other languages.

For example, you can assign to variables from a match.

let res = match work() {
  "foo" => "fooval",
  "bar" => "baval",
  _ => "defaultval",
}

The break keyword can return values too.

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {result}");
}

Outside of that it's pretty much as you'd expect from other languages.

view this post on Zulip Matthew Sanabria (Nov 08 2024 at 04:24):

Chapter 7 was all about error handling. Basically it comes down to 2 things.

There are many methods that one can call to work with a Result. For example, .is_ok() and .is_err() or .unwrap() and .expect().

Generally speaking though when you have a function that returns a Result<T, E> then you'll use the ? operator to turn this:

let foo = match do_work() {
  Ok(success_val) => success_val,
  Err(err) => return Err(err),
};

Into this:

let foo = do_work()?;

view this post on Zulip Matthew Sanabria (Nov 08 2024 at 04:25):

Chapter 8 covers crates and modules and all the things that goes with them. I'm halfway through the chapter but I wanted to write things down before going for a jog.

Crates are the container for a project's libraries and/or executables. Modules are how you can further organize code within a crate. This is kinda wild to me coming from Go because Go is the opposite- a module is the project source code and packages are the intra-module organization. All good though.

You can nest modules. Child modules will be able to access both public and private identifiers from parent modules but you still have to use imports (use) to do so. You can choose which identifiers to export using pub and its related pub(crate) and pub(in ./some/path) syntax. I didn't know about that last syntax so that was cool to see.

There are 3 ways to organize modules.

A module can re-export things it imports using pub use and there are keywords super and self to refer to parent modules or the current module respectively.

Rust has a concept of binary programs and library programs. You can cargo build --bin and carog build --lib respectively. Generally the crate's primary library is src/lib.rs and binaries are src/bin/foo.rs where foo is the resulting executable name.

Stopped there but ill cover attributes and tests later.

view this post on Zulip Matthew Sanabria (Nov 08 2024 at 04:25):

Chapter 8 finishes up with attributes and testing. Rust supports attributes that hint things to the compiler. One such attribute is #[test] to mark a function as a test or #[cfg(target_os = "macos")] to build the marked item on macOS only.

The #![FOO] syntax is used to mark everything in the current scope with the attribute.

Rust has built-in testing features that you can run using cargo test. Nothing too crazy to touch on here.

Rust support documentation comments much like Go, only their delimiter is ///. Code that's placed in doc comments is run during cargo test to ensure it compiles.

view this post on Zulip Matthew Sanabria (Nov 08 2024 at 04:25):

Chapter 9 is all about structs. If you're familiar with any C-like language you'll know what structs are and how to use them. The interesting part of Rust is that there's 3 types of structs- name-field structs, tuple-like structs, and unit-like structs. Coming from Go this is a bit different than what I'm used to.

// Named-field struct.
struct Person {
  name: String,
  age: usize,
}

// Tuple-like struct.
// Go doesn't have this.
struct 2DPoint(i32, i32);

// Unit-like struct.
struct Foo;

Methods on structs are defined using the impl keyword, which I kinda like honestly.

struct Person {
    first_name: String,
    last_name: String,
    age: usize,
}

impl Person {
    fn name(&self) -> String {
        format!("{} {}", self.first_name, self.last_name)
    }
}

Methods that don't use self as their first argument are type-associated functions.

struct Person {
    first_name: String,
    last_name: String,
    age: usize,
}

impl Person {
    // This is a type-associated function. It can only be
    // called like so: Person::new()
    fn new() -> Person {
        Person{
            String::from("Matthew"),
            String::from("Sanabria"),
            32,
        }
    }
}

Structs can also be private or public and their fields private or public. There's a concept called interior mutability which is essentially a pattern that's used to mutate data on a struct when there are already immutable references to that struct. There's a whole chapter on the official Rust book on this: https://doc.rust-lang.org/book/ch15-05-interior-mutability.html

view this post on Zulip Matthew Sanabria (Nov 08 2024 at 04:25):

Chapter 10 is about enums and patterns. Basically enums and pattern matching is the shit and a crucial part of Rust.

Enums are pretty wild in Rust. They can contain multiple different types.

enum Message {
    Quit,
    Write(String),
}

And even have methods attached to them using impl same like structs.

Pattern matching occurs with the match keyword and lets you exhaustively determine the value for a given enum. Pattern matching isn't limited to enums though, it's also how one would destructure or "unpack" values from structs, tuples, etc.

There are some more advanced features with patterns such as match guards (if statements for matches) and @ bindings but those are exceptions rather than rules.

view this post on Zulip Matthew Sanabria (Nov 08 2024 at 04:25):

Chapter 11 was a doozy! It was about traits and generics. There's A LOT of content in this chapter that I can't reasonably cover in detail.

There's a concept of trait objects and generics. Generally speaking we lean towards using generics but there are times where trait objects need the concept of dynamic dispatching which uses the dyn keyword. Basically the dyn keyword is used to say give me any type that implements this trait, but we don't know what those exact types will be at compile time so use dynamic dispatching to find out the right method to call on the given type at runtime.

I'll definitely be going back to this chapter as I write more Rust code. Here's a code example to summarize a bit.

trait Traveler {
    fn travel(&self) -> i32;
}

struct Person;
impl Traveler for Person {
    // People are slow. They travel 5 units.
    fn travel(&self) -> i32 {
        5
    }
}

struct Car;
impl Traveler for Car {
    // Cars are fast. They travel 99 units.
    fn travel(&self) -> i32 {
        99
    }
}

// Required a trait object to have a vector of any type that implements the Traveler trait. The Box
// is required because vectors must hold types of a consistent size.
fn total_distance(travelers: &[Box<dyn Traveler>]) -> i32 {
    travelers.iter().fold(0, |acc, t| acc + t.travel())
}

// Generic over type T that's any type that implements the Traveler trait.
fn extended_travel<T: Traveler>(t: &T) -> i32 {
    2 * t.travel()
}

fn main() {
    let p1 = Box::new(Person);
    let c1 = Box::new(Car);
    let travelers: Vec<Box<dyn Traveler>> = vec![p1, c1];
    println!("Total: {:?}", total_distance(&travelers));

    let p2 = Person;
    println!("Person: {}", extended_travel(&p2));
    let c2 = Car;
    println!("Car: {}", extended_travel(&c2));
}

view this post on Zulip Matthew Sanabria (Nov 08 2024 at 04:26):

Chapter 12 was about operator overloading. It focused on meta programming and how you can implement specific traits to customize the logic of operators like plus minus greater than less than.

It's pretty cool because in something like go, you don't have the capability so like what you can do is create like a custom structure that can use the plus operator to add two of those structures together or something.

What I found the most interesting is that under the hood it's all traits whereas in other languages it's a little bit different to implement this sort of meta programming .

view this post on Zulip Matthew Sanabria (Nov 08 2024 at 04:26):

Chapter 13 was listed of all of the common utility traits in the standard library. Traits like dropped sized clone copy and so on and so forth.

These are the traits that you would implement as like a library author if you wanted to make your code a little bit more resilient and native and rusty in the language.

view this post on Zulip Matthew Sanabria (Nov 08 2024 at 04:26):

Chapter 14 was about closures. It showed how closures can borrow variables or "steal" variables with move. What I found the most interesting was the concept that function types and closure types are different. That is, even if you have the same signature for both types one will not be able to be used where the other is accepted and vice versa. I also finally understand why we need Fn, FnOnce, and FnMut. Basically FnOnce is there for closures that drop values to guarantee they will only be called at most once. FnMut can be called multiple times and mutate the data it's passed. This becomes more valuable in multi-threaded programs and such.


Last updated: Dec 12 2024 at 16:20 UTC