Skip to main content

Rust

Introduction

Why Rust

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 📺

Dev Tools

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 📺

Variables

  • Use let to declare
  • Use snake_case
  • Immutable by default, so use let mut to declare as mutable
  • Rust can usually infer the type, but you can also include it in the declaration
  • Variable shadowing allows a variable name to be reassigned multiple times and its type to be changed

Functions

  • To decide whether an input should take a reference, ask “should the function take ownership of this input?” - If not, use a reference
fn own_input(arg: String)
fn borrow_input(arg: &String)

Numbers

  • u8 u16 u32 u64 u128 represent nonnegative 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

Strings

  • String values are stored on the heap
  • A slice is a reference to a range of 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
  • Working with strings in Rust • Amos Wenger 📖
  • Rust Pizza Slices, Arrays and Strings • Array and string slices • Code to the Moon 📺
// create a string
let mut s = String::from("hello world");
 
// copy a reference to chars 0-4 of s
let hello: &str = &s[..5]; // "hello"
 
// these are equivalent
let s_ref = &s;
let s_slice_full = &s[..];
 
// string literals are slices too
let s_literal = "hello world";
 
// arrays/vectors can also be sliced
let a = [1, 2, 3, 4, 5]; 
let slice = &a[1..3]; 
assert_eq!(slice, &[2, 3]);

Tuples

Structs

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 📺

Vectors

  • Vec • Rust Standard Library 📚

Macros

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 📺

Data Modeling

Async

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 📺

Debugging

  • println! macro
  • dbg! macro

Testing

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
    • mod keyword
    • pub keyword
  • 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 📕

Building CLIs

Building Web Backends

Building Web Frontends

Performance

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