Skip to main content

Rust

Introduction

Why Rust

  • Advanced mode will can unlock all the optimizations you could ever need
  • Easy mode can be as ergonomic as Python
  • It will teach you about pointers and other details about how memory works you may never learn from other languages
  • How to speak Rust • No Boilerplate 📺
  • Rust makes you feel like a GENIUS • No Boilerplate 📺
  • Rust Is Easy • The compiler teaches you • No Boilerplate 📺
  • Rust for the impatient • No Boilerplate 📺
  • What makes Rust different? • No Boilerplate 📺
  • Rust: Your code can be PERFECT • No Boilerplate 📺
  • Stop writing Rust • In other languages it’s easy to START projects, but in rust, it’s easy to FINISH them • No Boilerplate 📺
  • Rust makes cents • Rust saves cloud computing costs (by being extremely CPU and memory efficent) and the engineering time that would otherwise be spent resolving production bugs • No Boilerplate 📺
  • Rust is boring • Why Rust’s superior speed and reliability now make it the best language to choose for almost any project, especially backend web development • No Boilerplate 📺
  • Rust is not a faster horse • What makes Rust fundamentally different from C, C++ and other languages that came before it • No Boilerplate 📺
  • Rust is easy… (we make it hard) • Let’s Get Rusty 📺
  • When to Choose Rust • Nice walkthroughs of Rust’s comparative readability, safety, cloud CPU and memory cost savings, and supportive community • Tim McNamara 📺

Learning Rust

Using Rust in Easy Mode

  • Features like references, lifetimes and other optimizations are optional!
  • Rust can be as easy to use as Python for quick exploration and hacking if you wait to optimize
  • At first, explore quickly by using unwrap() and clone() everywhere
  • Later, optimize for safety by refactoring unwraps to enums to handle potential edge cases
  • Later, optimize for performance by refactoring clones to use references, lifetimes, etc if measurably necessary (which it likely won’t be)
  • How to learn Rust • At first, copy and clone everything (Rust is fast enough), pass owned variables in and out of functions, and generally avoid references (and therefore lifetimes and the borrow checker) unless the compiler tells you to use them • No Boilerplate 📺
  • Easy Mode Rust • At first, clone everywhere (e.g. clone the thing you’re iterate over), pass owned instances instead of references into functions, wrap function parameters with Arc<> if you need to mutate them, avoid implementing traits when possible (try to stick to deriving existing traits), avoid async if sync can possibly suffice, and see how far you can get with just the Vec and HashMap data structures • Andre Bogus 📕

General

Updates & Roadmap

Sources of Rust Content

  • Rust • YouTube channel 📺
  • Rust conference channels…
  • No Boilerplate • YouTube channel 📺
  • Let’s Get Rusty • YouTube channel 📺
  • Jon Gjengset • YouTube channel 📺
  • Chris Biscardi • YouTube channel 📺

Dev Tools

New Project Stack

Crates

  • polars • Manipulate tabular data • Polars 🛠️
  • rand • Generate random values 🛠️
  • rayon • Iterate in parallel 🛠️
  • regex - crates.io: Rust Package Registry
  • serde • Serialize and deserialize to/from JSON and other formats 🛠️
  • time • Measure and format date and time (see chrono if you need more) 🛠️

Variables

let type_inferred = 'immutable'
let type_explicit: &str = 'immutable'
let mut will_change = 'mutable'
 
// Variable names can be "shadowed"
let shadowed = 69 // unusable soon
let shadowed = true // type can change

Functions

// Every crate starts here
fn main() {}
 
// Arguments can be owned or borrowed
fn owns_input(arg: String) {}
fn borrows_input(arg: &String) {}
fn mutates_input(arg: &mut String) {}
 
// Functions can be unnamed
fn contains_closures() {
  let x = 1
  let y = 2
 
  // Both referred to as "closures"
  let closure = |z| x + y + z
  let anonymous = |z| z + 3
}

Types

Numbers

  • u8 u16 u32 u64 u128 represent non-negative whole numbers
  • i8 i16 i32 i64 i128 for representing whole numbers
  • pointer sized integers - usize isize for representing indexes and sizes of things in memory
  • floating point - f32 f64

Collections

Strings

// Create a string
let s: &str = "hello"; // a slice
let s: String = "hello".to_string();
let s: String = String::from("hello");
 
// Create a slice from a String
let s_ref = &s; // "hello"
let s_slice_full = &s[..]; // "hello"
let s_slice_part = &s[..4]; // "hell"
  • &str
  • String values are stored on the heap
  • A slice is a reference to a range of u8 bytes (ascii characters) in a String or items in a Vector
    • The range specifies the index of the first character and the index after the last character
    • The slice reference points directly to the substring data on the heap
    • A slice gains “read” and “own” permissions and removes the original string pointer’s “write” and “own” permissions
    • A slice includes a len property
    • One advantage of slices over index-based ranges is that the slice cannot be invalidated while it’s being used
  • Useful methods:
    • (similar to Vecs, since they are implemented as a Vec<u8>)
    • capacity() • How many bytes are allocated
    • chars().count()
    • pop() • Return Option<char> of last character
    • retain() • Same as filter, pass in a closure that takes a character and returns true if it should stay
    • shrink_to_fit() • Reduce memory allocated
  • Working with strings in Rust • Amos Wenger 📖
  • Rust Pizza Slices, Arrays and Strings • Array and string slices • Code to the Moon 📺

Vectors

  • Vec • Rust Standard Library 📚
  • Useful methods:
    • dedup • Remove sequential duplicates (sort first to remove all)
    • sort
    • sort_unstable
// Create an array slice
let a = [1, 2, 3, 4, 5]; 
let slice = &a[1..3]; 
assert_eq!(slice, &[2, 3]);

Enums

  • Enums are custom types that can be one of a set of enumerated values
  • Option<T> helps you use the type system to prevent errors
  • When enum values have data inside them, you can use match or if let to extract and use those values (depending on how many cases you need to handle)
    • If the function only has an effect in one condition, an if let is most idiomatic
    • If the function needs to return a value for each condition, a match is most appropriate
  • Enum variants are public by default
  • Enums and Pattern Matching • The Rust Programming Language 📕
  • Option • Rust Standard Library 📚
  • Either • Rust crate 📦
  • Rust’s Most Important Containers 📦 10 Useful PatternsOption and Result • Code to the Moon 📺

Structs

HashMap

  • hashbrown • More performant drop-in replacement for HashMap and HashSet

Iterators

Example from Learning Rust via Advent of Code:

let slot = units  
   .iter()
   .filter(|unit| self.is_enemy(u))                     // ignore non-enemies  
   .flat_map(|unit| unit.pos.adjacent_positions(w,h))   // iterate adjacent positions  
   .filter(|pos| !walls[pos] && !occupied[pos])         // ignore walls or occupied spaces 
   .min_by_key(|pos| (self.pos - pos).length_sq());     // pick closest slot
  • Iterator in std::iter - Rust
  • lazy
  • Methods:
    • collect - transform iterator into a collection; such as a Vec
    • count - counts iterations to reach end
    • cycle - creats iterator that loops
    • enumerate - creates iterator that returns tuple of iteration count and value
    • filter - uses closure to yield values
    • find - searches for value
    • flat_map - map plus flatten
    • fold - applies function to produce a single value
    • for_each - calls closure for each value
    • map - transforms each value into another
    • max - returns largest value in iteration
    • max_by - returns largest value using provided comparison function
    • min_by_key - returns value given a function
    • sum - returns sum of iterator values

Data Modeling

Control flow

match

  • The match Control Flow Construct • The Rust Programming Language 📕
  • Combining match and enums is useful in many situations: match against an enum, bind a variable to the data inside, then execute code based on it
  • Matches are exhaustive: the arms’ patterns must cover all possibilities
  • In the case of Option<T>, Rust ensures we explicitly handle the None case so we don’t assume we have a value that might be empty
  • The last match case can be a catch-all case for all remaining values (using a named variable like other if the value will be used or _ if it won’t be)
  • Match on a reference to the variable to avoid moving its ownership to the match handlers

if let

  • Concise Control Flow with if let • The Rust Programming Language 📕
  • Using if let means less typing, less indentation, and less boilerplate code; however, you lose the exhaustive checking that match enforces
  • Choosing between match and if let depends on whether gaining conciseness is an appropriate trade-off for losing exhaustive checking
  • Rust Branching - if let, match • Code to the Moon 📺

Error Handling

Ownership

  • Ownership is a big deal in Rust!
  • It’s a core concept that makes Rust fundamentally different from other languages
  • It results in the compiler forcing you to solve more potential errors ahead of time, in exchange for encountering fewer errors at runtime
  • Ownership Recap • The Rust Programming Language 📕
  • Ownership Inventory • The Rust Programming Language 📕

References

From The Rust Programming Language, Chapter 4.

  • References are non-owning pointers
  • They allow reading and writing data without consuming its ownership
  • References are created with borrows (& and &mut) and used with dereferences (*), often implicitly
  • Rust’s borrow checker enforces a system of permissions (read, write, own, flow) that ensures references are used safely:
    • Creating a reference will transfer (some or all) permissions from the borrowed path to the reference
    • Permissions are returned once the reference’s lifetime (usage) has ended
    • Data must outlive (not be dropped during the lifetime of) all references that point to it
      • If you want to pass around a reference to a value, you have to make sure that the underlying value lives long enough
      • Fix: use/return the original path instead of a reference to it (move ownership)
      • Fix: use/return a static literal value instead of a value allocated to the heap (if the data will never change)
      • Fix: defer borrow-checking to runtime by cloning a reference-counted pointer to the original path (via Rc::clone), which will wait to deallocate the data until the last Rc pointing to it has been dropped (opt into garbage collection)
      • Fix: shorten the borrower’s lifetime so the data can be safely changed/dropped
  • Copying a value without moving its ownership:
    • If a value is immutable and does not own heap data, it can be copied without moving its ownership
    • If a variable pointing to heap data could be copied without a move, then two variables could think they own the same data, leading to a double-free
    • Examples of types that own heap data (and therefore do not implement the Copy trait): String
    • Examples of types that do not own heap data (and therefore do implement the Copy trait): i32, &String
    • The compiler will not allow two mutable references to the same value to exist at the same time
    • Dereferencing a heap value tries to take ownership of it, but ownership cannot be taken through a reference (a non-owning pointer)
    • The compiler will fail with “cannot move out of…a shared reference” when trying to copy values stored on the heap to avoid two values thinking they own the same data, which would later result in trying to deallocate the same heap memory twice
    • Safe options for accessing heap values (options that don’t move ownership):
      • Clone the data
      • Use an immutable reference
    • Unsafe options for accessing heap values (options that would move ownership):
  • Rust Demystified 🪄 Simplifying The Toughest Parts • Demonstrating how references and lifetimes work by repeatedly refactoring a short code example • Code to the Moon 📺
  • Rust’s Alien Data Types 👽 Box, Rc, Arc • Examples of when using a smart pointer can solve a problem • Code to the Moon 📺
  • Rust Interior Mutability - Sneaking By The Borrow Checker • How to work around the limitations of the Rust borrow checker using Cell, RefCell, RwLock and Mutex • Code to the Moon 📺
  • What’s a reference in Rust? • Julia Evans 📖
  • Rust pattern: Rooting an Rc handle • Sometimes it is useful to clone the data you are iterating over into a local variable, so that the compiler knows it will not be freed; if the data is immutable, storing that data in an Rc or Arc makes that clone cheap (i.e., O(1)) • Baby Steps 📖

Async

Debugging

  • println! macro
  • dbg! macro
  • Easy Rust 195: Backtraces at runtime • Use std::env::set_var("RUST_BACKTRACE", "1") to enable compile time backtraces on panics + std::backtrace::Backtrace::capture() to capture at specific points (or force_capture() to capture regardless of the env var setting) • David MacLeod 📺

Testing

Observability

Modules, Crates, Packages & Workspaces

  • Rust lets you split a package into multiple crates and a crate into modules so you can refer to items defined in one module from another module by specifying absolute or relative paths
  • Paths can be brought into scope with a use statement so you can use a shorter path for multiple uses of the item in that scope
  • Module code is generally private by default, but you can make definitions public by adding the pub keyword
  • Paths: A way of naming an item, such as a struct, function, or module
    • absolute (starting with crate)
    • relative (possibly starting with super)
    • use keyword
      • shorten paths by defining a shortcut
      • for functions, create a shortcut to the parent module so function calls will start with it make it clear the function isn’t locally defined
      • for all other types, create a shortcut to the item itself
      • naming collisions can be solved either by stopping at the parent module or creating an alias with the as keyword
  • Modules and use: Let you control the organization, scope, and privacy of paths
  • Crates: A tree of modules that produces a library or executable
  • Packages: A Cargo feature that lets you build, test, and share crates
    • External packages are available at crates.io
    • Using them involves two steps: listing them in your package’s Cargo.toml and bringing their items into scope with use 
  • Managing Growing Projects with Packages, Crates and Modules • The Rust Programming Language 📕
  • Unboxing Rust Crates, Packages, Modules & Workspaces • Code to the Moon 📺
  • Rust API Guidelines • How to manage the changes you make to crates you’ve published for others to use • Rust 📕

Functional Paradigm

  • Rust’s Hidden Purity System • Particularly const functions • No Boilerplate 📺
  • Rust on Rails • How to write rust code that never crashes by using the Result type to implement the railway pattern, which eliminates null return values by enforcing error awareness and handling • No Boilerplate 📺

Macros

Performance

Cargo

Use Cases

Building CLIs

Building Web Backends

Building Web Frontends

Inbox

  • rust pros: it prioritizes safety, which I always focus on when using TS/python etc; I value spending time up front to prevent runtime bugs
  • Learning Rust via Advent of Code - Forrest Smith