< index

1 Yarr Notes

> yarr

Why am I doing this??

"No one is going to hire you to write rust" - people > 0

"Because a buddha is in birth and death, there is no birth and death… Because a buddha is not in birth and death, a buddha is not deluded by birth and death" - Eihei Dogen

I apologize for the many misspellings.

2 Yarr!

  • rust analyzer is very cool. I like how it looks. feels cozy and friendly.
  • Makes me think I should train an llm. Make a weird cameo study-buddy for myself.
  • basic cargo commands

run your thing. compile your thing and run.

cargo run
  • println!

this is macro syntax bruh.

here is src:

macro_rules! println {
    () => { ... };
    ($($arg:tt)*) => { ... };
}

wut. this looks like a match statement. like, are those two cases? $arg is suggestive. * reminds me of a wild card. { … } could mean anything.

same syntax as format! but prints to stdout instead.

bad in hot loops! what is a hot loop?

what is a hot loop???

what is a hot loop???

what is a hot loop???

io::stdout().lock();

wut?? OH!!! COOL!:

use std::io::{stdout, Write};

let mut lock = stdout().lock();
writeln!(lock, "hello world").unwrap();

BROOOOO. from std docks:

" Access is also synchronized via a lock and explicit control over locking is available via the lock method."

So a lock grants access. like, by definition, so. ok. The owner has the lock, no one else can access that resource.

example?

use std::io::{self, Write};

fn main() -> io::Result<()> {
    let mut stdout = io::stdout().lock();

    stdout.write_all(b"hello world")?;

    Ok(())
}

is it tho? I think it is just showing basic usage. not an exemplary case. Because I could just write hello world without a lock… right? Maybe missing something

  • eprintln! is for errors and progress messages (goes to stderr, i assume?).
  • check out the extreme cases on each side of the (optimized) spectrum on leetcode.

2.1 variables! (not a macro)

  • let mut! (also not a macro)
  • mutability means exclusive access. LIKE LOCKS. FOR DOORS.

2.1.1 Type annotations

  • statically typed lengua: we've all got types at compile time. woo.
  • type inference - compiler can figure some stuff out.
  • rust-analyzer tells you what types are inferred, which is SO helpful.

2.1.2 primitive types.

  • u8, u16, u32, u64, u128, usize. usize is a pointer-sized unsigned integer. used to index into collections. good to knoooooo.
  • i8 … i128, isize. isize is a pointer-sized signed integer. handy for representing the difference between two indexes. ah!
  • f32, f64. floating point ums. 1.2f32 or 1.2f64
  • char. 4 byes!
  • bool. hire me i know what a bool is!
  • str. utf-8 encoded string slice. &str. utf-8 makes accessing chars at different indices more complicated.
  • (). r/absoluteunit

2.1.3 tuples arrays

  • tuple (u8, bool)
  • arrays. fixed size. [u8; 5]

2.2 Casting

  • as <type>

2.3 Control Flow

  • 'blocks delimit a scope'
  • 'blocks return a value'
  • use blocks to release resources

cool

  • if
  • loop, while, for

2.4 fun

wow this resource was worth it just for introducing me to rust-analyzer.

2.5 memory management

  • stack! this is where you put plates. things on the stack only live as long as the function they are inside.
  • heap! this is where you find needles in O(1). things can live here until they are deallocated. this has to happen deliberately.
  • flavors: manual memory management, automatic memory management (garbage collection [java, go], reference counting [python, swift])
  • Rust does manual memory management via ownership. so neoclassical.

2.5.1 References and poincares.

  • x is x, &x is a reference to x, *x references the reference to the underlying value.
let x: u32 = 10;

let ref_x: &u32 = &x;

println!("x = {}", *ref_x);

Rust will usually dereference for you:

println!("x = {}", ref_x);
  • pointers are like references, but come with fewer guarantees. They are JUST an address in memory. so you can do unsafe things with them. Not really in the scope of yarr.

2.5.2 Heap Allocation

  • Box
let x: Box<u32> = Box::new(42);
let y = Box::<f64>::new(4.2);
  • deref that box:
let z = *x;

2.6 Collections

2.6.1 Vecs

let mut menu: Vec<&str> = Vec::new();
menu.push("meat");
println!("looks like {:?}'s back on the menu, boys!", menu);

let mut menu = vec!["meat"];

menu.push("meat");
menu.push("meat");
menu.push("meat");

for &menu_item in menu.iter() {
    println!("looks like {}'s back on the menu, boys!", menu_item);
}

2.6.2 other

  • HashMap
  • BTreeMap
  • HashSet
  • BTreeSet

2.7 Ownership and lifetimes

  • ownership is used for tracking when memory is valid and when it is dropped.
  • the lifetime of a variable is the time when references to it are valid.

2.8 Lifetimes

> "These are still things that disintegrate. What is the indestructible nature?" - some monk to Joshu

  • "Every reference is a borrow, and each borrow has a lifetime. That lifetime spans from when the variable is created to when it is destroyed."

Which is to say, if you have a reference to something, you're borrowing it (mutably or immutably). you can borrow it only so long as that thing exists. And thus a borrow has a lifetime spanning the birth and death of the thing.

  • the borrow checker makes sure every reference has a lifetime WHOLLY contained in the borrowed value's lifetime.
fn example<'a>(x: &'a u32) {
   let y: &'a u32 = &x;
}

does this mean y is a reference to a reference?

  • generics: <'a>
  • 'static means "referred to data will live for the duration of the program"

constants:

let msg: &'static str = "hello, wordl!";
  • anywhere type annotation can go, an explicit lifetime can go too.
  • lifetime elision. Compiler guesses based on rules.

2.9 Ownership

  • I think I misunderstood something before. Variables don't own things. Scopes own things and scopes borrow things. That's helpful.
  • copy. Some types implement the copy trait. These types types get copied instead of being moved, which means you can keep using it in both 'places'. ie:
let x: u32 = 10;
println!("x is {x}");
println!("x*2 is {}", x*2);

normally, println! would take ownership of x, meaning we couldn't reference x anymore after the completion of println!'s scope. But because x 'is Copy' here, we can keep using it because it is copied into the scope of println! instead of moved. Realizing this in vivo requires understanding the types and their respective traits as you pass them around.

2.10 Closures

  • these are essentially anonymous functions:
let y: u32 = 10;
let annotated = |x: u32| -> u32 { x + y };
let inferred = |x| x + y;

println!("annotated: {}", annotated(32));
println!("inferred: {}", inferred(32));
  • closures can reference values outside their scope. they can also capture and use them. By, say, getting a value from outside the scope, and incrementing it. But the closure must be mut if it captures a mutable variable. It's capturing a mutable variable (the variable becomes part of the closure) and it, thus the closure is changing.
  • functions can return closures:
fn print_msg<'a>(msg: &'a str) -> impl Fn() + 'a {
        let printer = move || {
          println!("{msg}");
        };
        printer
}

fn make_counter() -> impl FnMut() {
   let mut count = 0;
   let increment = move || {
       count += 1;
       count
   };
   increment
}
let adder = |a: u32, b: u32 | a + b;

2.11 Structs

  • struct fields
  • struct impl blocks
struct CrewMember {
  name: String,
  age: u32,
  odors: Vec<String>,
}

impl CrewMember {

  pub fn describe(&self) {

  println!("{} is {} and smells like {:?}", self.name, self.age, self.odors)

  }

}


2.12 Enums

  • good
  • variants: field by position; fields by name; unit variant
  • pattern matching
enum Weekdays {
     Monday,
     Tuesday,
     Wednesday,
     Thursday,
     Friday,
}

enum Result<T, E> {
     Ok(T),
     Err(E)
}

2.13 More control flow

let value = Some(42);

if let Some(inner) = value {
   println!("inner was {inner}");
} else {
   println!("no inner");
}

let vals = vec![1,2,3];
let mut iter = values.iter();

while let Some(v) = iter.next() {
   println!("v = {v}");
}

2.14 Modules

  • in a block delimited by braces:
pub mod math {

    pub fn add(x: u32, y: u32) -> u32 {
        x + y
    }
}

  • in a file at the level of main.rs with the name of the module (ie math.rs). must add pub mod math; to main.rs file.
  • in a folder (ie math) with a mod.rs file containing the code.

2.14.1 using modules

  • use!
  • use super::thing (parent module) or use crate::thing (from the root of the current crate)

3 Testing

  • create a child module called test.rs :
// only compile if the test compile option is on
#[cfg(test)]
mod tests {

    use super::*;

    #[test]
    fn adds_small_numbers() {
       let x = 10;
       let y = 20;
       let expected = 30;
       assert_eq!(plus(x, y), expected, "should add up correctly");
    }

    // this fails bc function params are i32 in the example.
    #[test]
    fn adds_big_numbers() {
       let x = 2_000_000_000;
       let y = 2_000_000_000;
       assert!(plus(x, y ) > 0, "result should be positive");
    }

}

3.0.1 integration tests

  • integration tests can only consume the public api of your library. ex:
use my_library::plus;

#[test]
fn test_addition() {
   assert_eq!(plus(10, 20), 30);
}

3.0.2 Doc tests

  • i hadn't heard of such a thing:
/// Adds things
///
/// ```
/// use playground::plus;
/// assert_eq!(30, plus(10,20));
/// ```
pub fn plus(x: i32, y: i32) -> i32 { x + y }

3.1 Linting and Formatting

  • formatter: cargo-fmt. run with: cargo fmt
  • linter: clippy. run with: cargo clippy
  • its usually good to treat clippy warnings as errors.

3.2 Dependency Management

  • crates.io
  • add a dependency to your project with (serde is an example): cargo add serde.
  • if you want a specific verion, add serde = "1.0.154" to Cargo.toml
  • semver: semantic versioning. MAJOR.MINOR.PATCH. changes to MAJOR imply incompatible API changes. Changes to MINOR imply functionality added in a backward-compatible manner. Changes to PATCH imply backward-compatible bug fixes.

3.3 Traits

  • for defining shared behavior
  • basically a collection of methods, either abstract or composed of other trait methods.
  • abstract methods are then implemented depending on the type

ex:

trait KeyValueStore {

// must be implemented by structs with the KeyValuestore trait
    fn set(&mut self, key: &str, value: Vec<u8>); // I think the value is just an example
    fn get(&mut self, key: &str);
    fn lock(&mut self, key: &str);
    fn unlock(&mut self, key: &str);

    // composed of the above methods
    fn get_and_set(&mut self, key: &str, value: Vec<u8>) -> Option<Vec<u8>> {

       self.lock(key);
       let old_value = self.get(key);
       self.set(key, value);

       self.unlock(key);
       old_value

    }
}

Note: everything is public by default.

3.3.1 example definition and example

trait Printable {
    fn print(&self);
}

struct Ship {
    name: String
}

impl Printable for Ship {
    fn print(&self) {
        println!("<Ship name=\"{}\">", self.name);
    }
}

  • some traits can also be derived:
#[derive(Debug, PartialEq)]
struct PirateShip {
    name: String,
    masts: f32, //1.5 masts apparently makes sense
}

  • you can impl any trait on a type you define. and you can impl a trait that you define on any type. BUT YOU CANNOT IMPL A TRAIT THAT YOU DIDN'T DEFINE ON A TYPE THAT YOU DIDN'T DEFINE. don't want multiple impls floating around.

3.4 using traits

  • use methods from a trait on a type that impls it.
  • accept the trait as parameters to a function or return type
  • use them as part of genenics

3.4.1 calling methods from traits

  • you must `use` a trait to make it visible ie:
use std::io::Read; // must include this

let mut s: Vec<u8> = "sad example".into();
let mut buf: [u8; 32] = [0; 32];
(&s[..]).read(&mut buf);

3.4.2 traits as parameters

  • this is not what I would consider a 'trait as parameter' but fine:
fn save_record(kv: &impl KeyValueStore) {
    //etc
}

to me, the trait in this case constrains the parameter, it is not the parameter itself. idk.

3.4.3 traits as return types

  • here the wording makes more sense. ex:
fn create_in_memory_kvstore(config: Config) -> impl KeyValueStore {
    todo!()
}

3.5 Generics

  • generics allow code that accepts one or more type parameters. ex:
fn max<T: PartialOrd>(x: T, y: T) -> T {
    if x > y {
       x
    } else {
       y
    }
}

fn main() {
    let a = 5;
    let b = 12;
    let c = 1.5;
    let d = 8.22;

    println!("Larger of {} and {}: {}", a, b, max(a, b));
    println!("Larger of {} and {}: {}", c, d, max(c ,d));

}

  • generic structs
struct Point<T> {
    x: T,
    y: T,
}

struct Point2<X, Y> {
    x: X,
    y: Y,
}

fn main() {

    let integer_point = Point { x = 1, y = 2};
    let float_point = Point { x = 1.2, y = 3.33};

    let mixed_point = Point2 { x = 1, y = 1.0}

}

  • generic enums and traits. ex:
use std::ops::Div;

enum Result<T, E> {
    Ok(T),
    Err(E),
}

// a 'generic' divide
fn divide<T: Div>(x: T, y: T) -> T::Output {
   x / y
} 

3.6 Error Handling

  • Result and the ? operator. If ? is used, the calling function must return a Result.
  • panics. reserved for exceptional situations where it is undesirable or impossible to continue executing the program. to be used sparingly and in exceptional circumstances. In most cases, Result is better.

3.7 Async/Await

  • concurrency and parallelism are not necessarily the same.
  • async defines asynchronous functions. they always return a Future. This is a value that is not available yet, but will be at some point. ex:
async fn fetch_data() -> Result<String, String> {
    // ...
}

This functions says it returns a Result, but this is rust hiding some details. The function doesn't return anything until you await it. When a future is awaited, the current task is suspended, allowing other tasks to run concurrently. ex:

async fn main() {
    match fetch_data().await {
        Ok(data) => println!("Data: {}", data),
        Err(error) => println!("Error: {}", error),
    }
}

  • async runtimes in rust: Tokio, but there are other options out there.

4 Fin

Author: yoni

Created: 2023-11-27 Mon 10:06

Validate