Wednesday | 13 NOV 2024
[ previous ]
[ next ]

Notes on the Rust Book

Title:
Date: 2024-11-07
Tags:  

Notes on Rust

Start a new project:

cargo new project_name

Run a project:

cargo run

Print something:

println!("Hello, World!");

Format strings:

let guess = 32
println!("Something: {guess}");

Include a library:

use std::io;

Create a mutable variable:

let mut guess = String::new();

Read user input:

io::stdin().read_line(&mut guess).expect("Failed to read line.");

To open docs for your specific dependencies:

cargo doc --open

Remove leading and trailing whitespace:

let guess = guess.trim();

Convert a string to a number:

let guess: u32 = guess.trim().parse().expect("Expected a number.");

Rust allows for shadowing so we have multiple declarations of something.

Error handling with match:

let guess: u32 = match guess.trim().parse() {
	Ok(num) => num,
	Err(_) => continue,
}

Learning rust again now is going better than the last time and the vibe is slightly better. We'll see if it holds.

I don't know if rust has changed or if I've forgotten but the print format taking the parameter directly is very useful thing. I remember always putting the variables at the end.

The match assignment makes error handling so obvious now. This has to be new but possibly its not. It makes error handling straightforward which was one of my biggest gripes. I remember panics and using ? being more common but that looks to have changed.

Common Ideas - Mutability and default immutable variables - Array out of bounds is checked at runtime and panicked on - Can't do x = y = 6 because assignment doesn't return anything in rust. - You can have a block as an assignment, this is cool. You can do quite a bit inside an assignment then. - if is an expression that returns a value so you can put this after an assignment - To return something from a loop you can use the break followed by some return data

How to a loop a range:

for number in (1..4).rev() {
	println!("{number}");
}

The expressions stuff is pretty cool in rust, with conditionals being expressions the assignment logic looks really interesting. let followed by an if or a loop is a neat idea that I want to try.

You can use a match statement to trigger things conditionally, similar to a case statement in my eyes or a switch. The catchall is either something like other or using _ when you don't plan to use the variable.

Match statement:

match num {
	3 => Do1(),
	7 => Do2(),
	other => Do3(other),
}

The if let style is a shortcut to the match statement. This way you can handle just what you want.

let config_max = Some(3u8);
if let Some(max) = config_max {
	println1("Config is {max}");
}

Dynamic arrays are vectors.

let v: Vec<i32> = Vec::new();

Add to an array:

v.push(5);

Access can be done using 2 ways:

let x = &v[2];

let x = v.get(2);

The get option returns an Option type.

The first option will panic. This seems like a bad idea but probably useful when you know for sure it won't be over.

Looping over an array:

let v = vec![100,32,56];
for i in &v {
	println!("{i}");
}

Remove an element from an array:

v.remove(index);

Check if something is in an array:

v.contains(&x);

v.iter().any(|&i| i == "something");

Find the position of something in a vector:

v.iter().position(|&r| r == "something").unwrap();

A vector has to hold the same type of data but you can bundle data with an enum.

Strings can be concatenated:

s3 = s1 + & &s2;

This seems to be saying that s1 gets moved but s2 continues to live on.

Damn, rust doesn't support have field access in the string. It's not a templating thing, it's a named parameter.

read_to_string takes ownership of the variable so you need to use a reference or clone the file path.

We always need lifetimes when we pass in two references. The return needs to be tied to something.

Move lets you move variables into a thread.

Rust iterators are zero cost abstractions. Very cool idea.

Rust's core idea is ownership and this is what removes the need for memory management.

Rust makes certain assumptions, one key being that variables will go out of scope and their memory should be deallocated. This is the same pattern as the C++ RAII pattern of design.

Rust also moves ownership on assignment. This means that once you assign a complex item to another variable, the first one has lost a connection to the data. This is only for heap allocated objects with unknown sizes at compile time. Stack allocated variables have a known type and so are duplicated on assignment.

Stack operated items have the Copy trait, and the Copy trait is invalid when used with the Drop trait. This is a cool idea, you can build a struct with variables of fixed size and add the copy trait I'm guessing as long as you don't do something special in the drop trait. Clone and copy require their constituent parts to have the same traits. This way if the parts have it, then the whole can also have the traits. Which makes sense. Calling clone will then call all the parts' clones.

This logic is also why its called Copy and Clone. Copy is for the stack while Clone is for heap allocated data.

Copy a string:

let newString = s1.clone();

I must've known but it feels new. Passing a variable into a function causes a move and that variable will then go out of scope. This is crazy. Probably why we pass by reference when we don't want the variable moved. The mut now also makes sense when we want to pass something by reference and make a change to it inside the function.

Returning from a function is also a transfer of ownership. Instead of the reference, you can use the move to move the variable to the function scope and then return the variable back. This will move the variable back to the calling scope.

Passing by reference is called borrowing in rust.

An immutable variable can be tracked and seen where it is used. This means that a mutable reference can be created if the immutable reference has stopped being used as long as the mutable reference is after the all immutable references.

Loop around a string:

let bytes = s1.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
}

Clear a string:

s1.clear();

The type of a string literal is &str because it is a reference of the hardcoded string value.

Structs can have the Display and Debug trait.

Impl is the method syntax. This is how we can add functions to a struct. Methods have the self variable. Associated functions are things like constructors and use the :: syntax as they don't have an instance to use the dot syntax.

struct Rectangle {
	width: u32,
	height: u32,
}
impl {
	fn area(&self) {
		self.width * self.height
	}
}

Enums can contain data inside the Enum. That's pretty useful. I think this mixed with match would probably handle editing files. You have a Change Enum that can be an Update/Delete and each could have different data. Then when you do change.run(), it would run different code based on the Enum. That's pretty useful and possibly better than a struct.

You can also add methods to enums with impl.

Enums are basically structs with a union.

Errors can be handled with panic, match statements, closures and the question mark. There is also unwrap, unwrap or else and expect.

If you find yourself adding checks to test some limit, it might be better to make it it's own type. This way checking will happen on creation and you control how the type is used.

Rust also has support for generics. Generics let you abstract the type. We can restrict the types by using traits. This way only types with certain traits can use the generic function. Generic methods can be defined per type, so not everything has to be shared.

Rust generics are monomorphization, which means that the generics are expanded for all the concrete types that the program uses.

Traits are a set of methods. If something has that set of methods then it can be used the same way as something else that shares those set of methods. We can then use traits to narrow down functions to only accept types that implement that trait.

Lifetime rules are another area that is new in rust. You can specify the lifetimes expected in a function and the compiler will make sure these match. The lifetimes is how long variables will leave and how their scopes relate. The book has a really good example that I will duplicate here.

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {result}");
}

Lifetime annotations are just annotations, they are compiler hints and don't actually do anything in the program. They enforce certain ideas.

Testing

cargo new project --lib

cargo test

You can actually also write documentation code and have it get executed as well. Cool.

We can also use [should_panic] to test error routes. We can also use expected to make sure we get the correct error panic.

Rust code should be a binary with a library crate that can be tested. Binaries aren't tested.

Object oriented programming chapter was long and not really that fun. I read it but I don't think I took any of it in.

Pattern matching is pretty solid. Interestingly you destructure structs and deeply nested structs. This looks very useful. It can also be done inside of match statements and I imagine would be the type of thing that I would want someone to point out to me. I would write the match statement with a large brace because of the nested data but it can be destructured in the pattern.

let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });

Match gaurds are useful when you need to match a pattern and also need to do some checking on the variable before choosing which arm to go to.