Skip to main content

Rust

Introduction

Why Rust

  • Advanced mode can unlock all the optimizations you’ll ever need, while easy mode can be as ergonomic as Python
  • It makes you a better programmer by making you aware of how your variables are stored in memory
  • 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 📺
  • When to choose Rust • Nice walkthrough of Rust’s comparative readability, safety, cloud CPU and memory cost savings, and supportive community • Tim McNamara 📺
  • Considering Rust • Jon Gjengset 📺
  • Rust is easy… (we make it hard) • Let’s Get Rusty 📺

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 📕
  • From Python to Rust. Should I be worried about Rust’s complexity in the future? • Reddit 💬
  • How useful is Rust for quick prototyping++? • Top level anyhow::Result, then use ? on all Result instances to send all errors to the top level • Reddit 💬

General

Updates & Roadmap

Sources of Rust Content

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

New Project

Dev Tools

Project Stack

Crates

  • Crate List • Unofficial guide to the Rust ecosystem • Blessed.rs 📚
  • 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) 🛠️

Language features

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
}

Data 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 📖
  • Small strings in Rust • Amos Wenger 📖
  • Rust Pizza Slices, Arrays and Strings • Array and string slices • Code to the Moon 📺
  • String vs &str in Rust functions • Herman J. Radtke III 📖
  • Creating a Rust function that accepts String or &str • Herman J. Radtke III 📖
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 📺
  • Replace Dicts with Rust Enums • Herman J. Radtke III 📖
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
  • Improving Rust code with combinators • Let’s Get Rusty 📺
  • Iterating over Option • Let’s Get Rusty 📺
  • Rust’s iterators optimize nicely—and contain a footgun • Iterators read like they’re iterating multiple times, but they actually compile down to a single for loop that performs one iteration that composes your actions at each step; that’s a huge performance win but can be a subtle bug in cases where you actually need to iterate multiple times • Nicole Tietz 📖

Generics

Smart Pointers

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
  • For any type T, you have T, &mut T and &T:
    • T is owned
    • &mut T is exclusive
    • &T is shared
  • At compile time, Rust will check that you never have these at the same time:
    • &mut T and &T
    • multiple &mut T
  • It’s fine to have multiple &T
  • This is all Rust needs in order to prove your code does not have any data races (i.e. instances where one part of your code tries to read or update a value that another part of your code has updated or deleted)
  • A Mutex provides a safe way to mutate (get &mut T) shared data (via “locks”), but they are slow because they force sequential access to data and disallow concurrency
    • A RWLock allows concurrent reads, but involves taking and releasing read locks, which is fine if you then do a long process; but if you do not much, then taking/releasing the locks will become a bottleneck
  • The Rust Survival Guide • A clear, brief intro to Rust’s ownership and borrowing rules • Let’s Get Rusty 📺
  • Ownership Recap • The Rust Programming Language 📕
  • Ownership Inventory • The Rust Programming Language 📕
  • Declarative memory management • Amos Wenger 📖
  • Rust Means Never Having to Close a Socket • Yehuda Katz 📖

References

From The Rust Programming Language, Chapter 4.

  • References are non-owning pointers
  • They are like pointers in other languages, but with additional contracts (restrictions)
  • 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 📖

Lifetimes

Concurrency

  • Concurrent vs parallel

Threads

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 📕
  • Rust Modules - Explained Like I’m 5 • Refactors a single-module crate into a file with multiple modules, then multiple files with one module each • Let’s Get Rusty 📺

Programming Paradigms

Functional

  • 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 📺

Object-Oriented

  • Class Based OOP vs Traits • Nice examples of how Java OOP code would be translated to Rust’s structs and traits • Let’s Get Rusty 📺

Macros

Optimizing

Performance

Memory Usage

  • Vectors - optimization ideas:
    • Define maximum capacity up front (if known)
    • Use an array instead

Cargo

Use Cases

Cross-Platform

Command Line

Bash replacement

Web Backend

Web Frontend

HTTP request

Games

  • Bevy

Comparison to Python

Inbox