The Rust Programming Language

by Steve Klabnik, Carol Nichols, and Chris Krycho, with contributions from the Rust Community

This version of the text assumes you’re using Rust 1.85.0 (released 2025-02-17) or later with edition = "2024" in the Cargo.toml file of all projects to configure them to use Rust 2024 edition idioms. See the “Installation” section of Chapter 1 to install or update Rust.

The HTML format is available online at https://doc.rust-lang.org/stable/book/ and offline with installations of Rust made with rustup; run rustup doc --book to open.

Several community translations are also available.

This text is available in paperback and ebook format from No Starch Press.

🚨 Want a more interactive learning experience? Try out a different version of the Rust Book, featuring: quizzes, highlighting, visualizations, and more: https://rust-book.cs.brown.edu

Foreword

It wasn’t always so clear, but the Rust programming language is fundamentally about empowerment: no matter what kind of code you are writing now, Rust empowers you to reach farther, to program with confidence in a wider variety of domains than you did before.

Take, for example, “systems-level” work that deals with low-level details of memory management, data representation, and concurrency. Traditionally, this realm of programming is seen as arcane, accessible only to a select few who have devoted the necessary years learning to avoid its infamous pitfalls. And even those who practice it do so with caution, lest their code be open to exploits, crashes, or corruption.

Rust breaks down these barriers by eliminating the old pitfalls and providing a friendly, polished set of tools to help you along the way. Programmers who need to “dip down” into lower-level control can do so with Rust, without taking on the customary risk of crashes or security holes, and without having to learn the fine points of a fickle toolchain. Better yet, the language is designed to guide you naturally towards reliable code that is efficient in terms of speed and memory usage.

Programmers who are already working with low-level code can use Rust to raise their ambitions. For example, introducing parallelism in Rust is a relatively low-risk operation: the compiler will catch the classical mistakes for you. And you can tackle more aggressive optimizations in your code with the confidence that you won’t accidentally introduce crashes or vulnerabilities.

But Rust isn’t limited to low-level systems programming. It’s expressive and ergonomic enough to make CLI apps, web servers, and many other kinds of code quite pleasant to write — you’ll find simple examples of both later in the book. Working with Rust allows you to build skills that transfer from one domain to another; you can learn Rust by writing a web app, then apply those same skills to target your Raspberry Pi.

This book fully embraces the potential of Rust to empower its users. It’s a friendly and approachable text intended to help you level up not just your knowledge of Rust, but also your reach and confidence as a programmer in general. So dive in, get ready to learn—and welcome to the Rust community!

— Nicholas Matsakis and Aaron Turon

Introduction

Note: This edition of the book is the same as The Rust Programming Language available in print and ebook format from No Starch Press.

The Rust Programming Language provides a systems programming approach that combines memory safety with zero-cost abstractions. Unlike TypeScript’s runtime type checking or Node.js’s garbage-collected memory management, Rust enforces safety at compile time without runtime overhead.

Rust addresses common pain points in systems programming: memory leaks, race conditions, and undefined behavior—issues that higher-level languages like TypeScript abstract away but become critical in systems-level development.

Why Rust for Experienced Engineers

Memory Safety Without Performance Cost

Rust’s ownership system eliminates entire classes of bugs common in C/C++ while matching their performance. Think of it as compile-time guarantees for what you’d normally handle with careful code reviews and runtime checks in other languages.

Modern Tooling Ecosystem

Rust’s development experience rivals modern web development:

  • Cargo: Dependency management and build system (similar to npm/yarn but for systems programming)
  • Rustfmt: Code formatting (like Prettier)
  • rust-analyzer: LSP implementation providing IDE features comparable to TypeScript’s language server
  • Clippy: Static analysis tool beyond basic linting

Concurrency Without Data Races

Rust’s ownership model prevents data races at compile time—eliminating a major source of production bugs in concurrent systems. This is particularly relevant coming from Node.js’s event loop model where race conditions are less common but not impossible.

Production Adoption

Major companies use Rust for performance-critical infrastructure: Discord (voice/video), Dropbox (storage), Facebook (source control), Microsoft (Windows components), and Mozilla (Firefox). These are systems where TypeScript/Node.js wouldn’t be appropriate due to performance requirements.

Target Use Cases

  • CLI tools: Better performance than Node.js scripts with similar ergonomics
  • Web services: Lower latency and memory usage than Node.js/TypeScript backends
  • Systems programming: Device drivers, embedded systems, OS components
  • Performance-critical components: Computational algorithms, real-time systems
  • Infrastructure tooling: Networking tools, databases, container runtimes

Book Structure

This book assumes software engineering experience. Chapters are divided into concept and project chapters:

Core Concepts (Chapters 1-11):

  • Ch 1: Installation and tooling setup
  • Ch 2: Hands-on number guessing game (quick practical intro)
  • Ch 3: Basic syntax and types
  • Ch 4: Ownership system (Rust’s key differentiator—most important chapter)
  • Ch 5-6: Structs and enums (similar to TypeScript interfaces/unions but with more capabilities)
  • Ch 7: Module system (package organization)
  • Ch 8: Collections (Vec, HashMap, etc.)
  • Ch 9: Error handling (Result types—different from exceptions)
  • Ch 10: Generics and traits (similar to TypeScript generics but more powerful)
  • Ch 11: Testing

Applied Concepts (Chapters 12-21):

  • Ch 12: CLI tool project (grep implementation)
  • Ch 13: Functional programming features (closures, iterators)
  • Ch 14: Cargo ecosystem and publishing
  • Ch 15: Smart pointers (memory management primitives)
  • Ch 16: Concurrency (threads, channels, async)
  • Ch 17: Async/await (different from JavaScript promises but similar concepts)
  • Ch 18: Object-oriented patterns in Rust
  • Ch 19: Pattern matching (more powerful than switch statements)
  • Ch 20: Advanced features (unsafe code, macros)
  • Ch 21: Multithreaded web server project

Recommended approach: Read sequentially through Chapter 4 (ownership is fundamental), then adapt based on your specific interests.

Error Messages and Learning Process

Rust’s compiler provides detailed error messages with suggested fixes. Unlike TypeScript’s sometimes cryptic errors, Rust errors are designed to be educational. The compiler often suggests exactly what to change.

Code examples use Ferris icons to indicate expected behavior:

IconMeaning
Ferris with a question markCompilation failure (intentional for learning)
Ferris throwing up their handsRuntime panic (controlled failure)
Ferris with one claw up, shruggingRuns but produces incorrect results

Source Code

The source files from which this book is generated can be found on GitHub.

Getting Started

This chapter covers essential setup and tooling for Rust development:

  • Installation: Rust toolchain via rustup
  • Hello World: Basic compilation and execution
  • Cargo: Package manager and build system (Rust’s equivalent to npm/yarn)

Installation

Install Rust via rustup, the official toolchain manager that handles Rust versions, targets, and associated tools.

Install rustup

Unix/macOS:

$ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

Windows: Download and run the installer from https://www.rust-lang.org/tools/install

The installer includes:

  • rustc (compiler)
  • cargo (package manager/build system)
  • rustfmt (code formatter)
  • clippy (linter)
  • Local documentation

Dependencies

macOS:

$ xcode-select --install

Ubuntu/Debian:

$ sudo apt install build-essential

Windows: Visual Studio Build Tools or Visual Studio with C++ development tools.

Verification

$ rustc --version
$ cargo --version

Management Commands

$ rustup update              # Update to latest stable
$ rustup self uninstall      # Remove rustup and Rust
$ rustup doc                 # Open local documentation

IDE Setup

Recommended: VS Code with rust-analyzer extension (provides LSP features comparable to TypeScript’s language server).

Offline Development

For projects requiring external dependencies:

$ cargo new get-dependencies
$ cd get-dependencies
$ cargo add rand@0.8.5 trpl@0.2.0

Use --offline flag with cargo commands to use cached dependencies.

Hello, World!

Direct Compilation

Create main.rs:

fn main() {
    println!("Hello, world!");
}

Compile and run:

$ rustc main.rs
$ ./main                    # Linux/macOS
$ .\main.exe               # Windows

Program Structure

fn main() {                 // Entry point (like main() in C)
    println!("Hello, world!");  // Macro call (note the !)
}

Key differences from TypeScript/Node.js:

  • Ahead-of-time compilation: Unlike Node.js’s JIT, Rust compiles to native binaries
  • Macros: println! is a macro (indicated by !), not a function
  • No runtime: Unlike Node.js, compiled binaries run without a runtime environment

Compilation Model

Unlike interpreted languages (JavaScript) or VM languages with JIT (Java), Rust uses ahead-of-time compilation:

  1. SourceBinary: Direct compilation to machine code
  2. Zero dependencies: Binaries run without Rust installation
  3. Static linking: All dependencies bundled in the executable

This is similar to Go’s compilation model but with stronger memory safety guarantees.

For larger projects, use Cargo instead of direct rustc compilation.

Hello, Cargo!

Cargo is Rust’s integrated build system and package manager—equivalent to npm/yarn but with built-in compilation, testing, and project management.

Project Creation

$ cargo new hello_cargo
$ cd hello_cargo

Generated structure:

hello_cargo/
├── Cargo.toml          # Package manifest (like package.json)
├── .gitignore          # Git ignore file
└── src/
    └── main.rs         # Source code

Cargo.toml

[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2024"        # Rust edition (language version)

[dependencies]          # Dependencies (like package.json dependencies)

vs package.json:

  • TOML format instead of JSON
  • edition field specifies Rust language features
  • Dependencies are automatically compiled, not interpreted

Essential Commands

$ cargo build           # Compile (creates target/debug/)
$ cargo run             # Build + execute
$ cargo check           # Type check without generating binary
$ cargo build --release # Optimized build (target/release/)
$ cargo test            # Run tests
$ cargo doc --open      # Generate and open documentation

Development Workflow

Debug builds (default):

  • Fast compilation
  • Includes debugging symbols
  • No optimizations
  • Located in target/debug/

Release builds:

  • Slower compilation
  • Full optimizations
  • Smaller binaries
  • Located in target/release/

Key Differences from npm/yarn

Featurenpm/yarnCargo
InstallationRuntime dependency resolutionCompile-time linking
ExecutionRequires Node.js runtimeStandalone binary
VersioningSemantic versioningSemantic versioning + edition system
Lock filepackage-lock.json/yarn.lockCargo.lock
Scriptspackage.json scriptsBuilt-in commands

Cargo.lock

Automatically generated dependency lock file (like package-lock.json). Ensures reproducible builds across environments. Commit to version control for applications, not for libraries.

Integration with Existing Projects

$ git clone <repo>
$ cd <project>
$ cargo build          # Downloads deps, compiles everything

No separate dependency installation step—Cargo handles everything in the build process.

Cargo becomes essential for:

  • Dependency management
  • Multi-file projects
  • Testing and documentation
  • Publishing to crates.io (Rust’s package registry)

Programming a Guessing Game

This chapter demonstrates core Rust concepts through a practical project: a CLI guessing game. Unlike Node.js scripts, this compiles to a self-contained binary with no runtime dependencies.

Key concepts covered:

  • Variables and mutability (let vs let mut)
  • Pattern matching (match expressions)
  • Error handling (Result type vs exceptions)
  • External dependencies (Cargo vs npm)
  • Type system (compile-time safety vs runtime checks)

Project Setup

$ cargo new guessing_game
$ cd guessing_game

Generated structure matches standard Rust conventions:

# Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2024"

[dependencies]

Input Handling

Unlike Node.js’s readline or process.stdin, Rust uses explicit imports and error handling:

use std::io;

fn main() {
    println!("Guess the number!");
    println!("Please input your guess.");

    let mut guess = String::new();  // Mutable variable (explicit)
    
    io::stdin()
        .read_line(&mut guess)      // Borrows mutable reference
        .expect("Failed to read line");  // Error handling (no try/catch)

    println!("You guessed: {guess}");
}

Key differences from TypeScript/Node.js:

  • Explicit mutability: let mut vs let (immutable by default)
  • Borrowing: &mut guess passes a mutable reference, not the value
  • No exceptions: Result type forces explicit error handling
  • No garbage collection: Memory managed through ownership system

External Dependencies

Add the rand crate to Cargo.toml:

[dependencies]
rand = "0.8.5"

vs npm/package.json:

  • Dependencies compile into the binary (no node_modules at runtime)
  • Semantic versioning with automatic compatibility resolution
  • Cargo.lock ensures reproducible builds (like package-lock.json)
$ cargo build  # Downloads, compiles, and links dependencies

Random Number Generation

use rand::Rng;
use std::io;

fn main() {
    println!("Guess the number!");
    
    let secret_number = rand::thread_rng().gen_range(1..=100);
    // Unlike Math.random(), this is cryptographically secure by default
    
    println!("Please input your guess.");
    
    let mut guess = String::new();
    io::stdin().read_line(&mut guess).expect("Failed to read line");
    
    println!("You guessed: {guess}");
    println!("The secret number was: {secret_number}");
}

Pattern Matching and Comparison

Rust’s match is more powerful than TypeScript’s switch:

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    let secret_number = rand::thread_rng().gen_range(1..=100);
    
    println!("Please input your guess.");
    let mut guess = String::new();
    io::stdin().read_line(&mut guess).expect("Failed to read line");
    
    // Type conversion with error handling
    let guess: u32 = guess.trim().parse().expect("Please type a number!");
    
    println!("You guessed: {guess}");
    
    // Pattern matching (exhaustive)
    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

Key concepts:

  • Type conversion: parse() returns Result<T, E>, not T | undefined
  • Exhaustive matching: Compiler ensures all cases are handled
  • No type coercion: Must explicitly convert String to u32

Game Loop with Robust Error Handling

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Guess the number!");
    let secret_number = rand::thread_rng().gen_range(1..=100);
    
    loop {  // Infinite loop (like while(true) but more idiomatic)
        println!("Please input your guess.");
        
        let mut guess = String::new();
        io::stdin().read_line(&mut guess).expect("Failed to read line");
        
        // Graceful error handling instead of throwing exceptions
        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => {
                println!("Please type a number!");
                continue;  // Skip to next iteration
            }
        };
        
        println!("You guessed: {guess}");
        
        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;  // Exit loop
            }
        }
    }
}

Final Implementation

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Guess the number!");
    let secret_number = rand::thread_rng().gen_range(1..=100);
    
    loop {
        println!("Please input your guess.");
        
        let mut guess = String::new();
        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");
        
        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };
        
        println!("You guessed: {guess}");
        
        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

Key Rust Concepts Demonstrated

Memory Safety:

  • No null pointer dereferences (no null or undefined)
  • Borrowing prevents data races at compile time
  • No manual memory management needed

Type System:

  • Static typing with inference (like TypeScript but stricter)
  • Result<T, E> for error handling (instead of exceptions)
  • Pattern matching ensures exhaustive case handling

Performance:

  • Zero-cost abstractions (no runtime overhead)
  • Compiled binary runs without runtime environment
  • No garbage collection pauses

Error Handling:

  • Explicit error handling with Result and Option types
  • match expressions for control flow
  • Compile-time guarantees about error handling

This approach differs significantly from TypeScript/Node.js patterns and demonstrates Rust’s emphasis on safety, performance, and explicit error handling.

Common Programming Concepts

This chapter covers Rust’s implementation of core programming concepts: variables, types, functions, comments, and control flow. While these concepts exist in most languages, Rust’s approach has specific characteristics around ownership, memory safety, and type strictness.

Keywords

Rust reserves specific keywords that cannot be used as identifiers. See Appendix A for the complete list.

Variables and Mutability

Variables are immutable by default in Rust. This design choice enforces memory safety and prevents data races in concurrent contexts.

fn main() {
    let x = 5;
    println!("The value of x is: {x}");
    x = 6;
    println!("The value of x is: {x}");
}

This produces a compile-time error:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
error[E0384]: cannot assign twice to immutable variable `x`
 --> src/main.rs:4:5
  |
2 |     let x = 5;
  |         - first assignment to `x`
3 |     println!("The value of x is: {x}");
4 |     x = 6;
  |     ^^^^^ cannot assign twice to immutable variable
  |
help: consider making this binding mutable
  |
2 |     let mut x = 5;
  |         +++

For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables` (bin "variables") due to 1 previous error

To allow mutation, use the mut keyword:

fn main() {
    let mut x = 5;
    println!("The value of x is: {x}");
    x = 6;
    println!("The value of x is: {x}");
}

Constants

Constants differ from immutable variables in several ways:

  • Always immutable (no mut allowed)
  • Declared with const keyword
  • Type annotation required
  • Must be set to compile-time constant expressions
  • Can be declared in any scope, including global
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

Constants use SCREAMING_SNAKE_CASE by convention and are valid for the entire program duration within their scope.

Shadowing

Rust allows variable shadowing - declaring a new variable with the same name:

fn main() {
    let x = 5;

    let x = x + 1;

    {
        let x = x * 2;
        println!("The value of x in the inner scope is: {x}");
    }

    println!("The value of x is: {x}");
}

Output:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/variables`
The value of x in the inner scope is: 12
The value of x is: 6

Shadowing differs from mutation:

  • Creates a new variable (enables type changes)
  • Requires let keyword
  • Variable remains immutable after transformations
fn main() {
    let spaces = "   ";
    let spaces = spaces.len();
}

This is invalid with mut:

fn main() {
    let mut spaces = "   ";
    spaces = spaces.len();
}

Error:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
error[E0308]: mismatched types
 --> src/main.rs:3:14
  |
2 |     let mut spaces = "   ";
  |                      ----- expected due to this value
3 |     spaces = spaces.len();
  |              ^^^^^^^^^^^^ expected `&str`, found `usize`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `variables` (bin "variables") due to 1 previous error

Data Types

Rust is statically typed - all variable types must be known at compile time. The compiler can usually infer types, but explicit annotations are required when multiple types are possible:

let guess: u32 = "42".parse().expect("Not a number!");

Without the type annotation:

$ cargo build
   Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0284]: type annotations needed
 --> src/main.rs:2:9
  |
2 |     let guess = "42".parse().expect("Not a number!");
  |         ^^^^^        ----- type must be known at this point
  |
  = note: cannot satisfy `<_ as FromStr>::Err == _`
help: consider giving `guess` an explicit type
  |
2 |     let guess: /* Type */ = "42".parse().expect("Not a number!");
  |              ++++++++++++

For more information about this error, try `rustc --explain E0284`.
error: could not compile `no_type_annotations` (bin "no_type_annotations") due to 1 previous error

Scalar Types

Rust has four primary scalar types: integers, floating-point numbers, booleans, and characters.

Integer Types

Table 3-1: Integer Types in Rust

LengthSignedUnsigned
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
archisizeusize

Signed variants store numbers from −(2n − 1) to 2n − 1 − 1, unsigned from 0 to 2n − 1. isize and usize depend on target architecture (64-bit on 64-bit systems).

Integer literals:

Table 3-2: Integer Literals in Rust

Number literalsExample
Decimal98_222
Hex0xff
Octal0o77
Binary0b1111_0000
Byte (u8 only)b'A'

Default integer type is i32. Use isize/usize for collection indexing.

Integer Overflow

In debug mode, integer overflow causes panics. In release mode (--release), Rust performs two’s complement wrapping. Handle overflow explicitly with:

  • wrapping_* methods (e.g., wrapping_add)
  • checked_* methods (return Option)
  • overflowing_* methods (return value + overflow boolean)
  • saturating_* methods (clamp to min/max)

Floating-Point Types

Two floating-point types: f32 and f64 (default). Both are signed and follow IEEE-754 standard.

fn main() {
    let x = 2.0; // f64

    let y: f32 = 3.0; // f32
}

Numeric Operations

Standard mathematical operations: +, -, *, /, %. Integer division truncates toward zero.

fn main() {
    // addition
    let sum = 5 + 10;

    // subtraction
    let difference = 95.5 - 4.3;

    // multiplication
    let product = 4 * 30;

    // division
    let quotient = 56.7 / 32.2;
    let truncated = -5 / 3; // Results in -1

    // remainder
    let remainder = 43 % 5;
}

Boolean Type

bool type with values true and false. One byte in size.

fn main() {
    let t = true;

    let f: bool = false; // with explicit type annotation
}

Character Type

char type represents Unicode scalar values (4 bytes). Use single quotes for char literals, double quotes for strings.

fn main() {
    let c = 'z';
    let z: char = 'ℤ'; // with explicit type annotation
    let heart_eyed_cat = '😻';
}

Valid Unicode scalar values: U+0000 to U+D7FF and U+E000 to U+10FFFF.

Compound Types

Tuple Type

Fixed-length collection of values with heterogeneous types:

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}

Access elements by destructuring:

fn main() {
    let tup = (500, 6.4, 1);

    let (x, y, z) = tup;

    println!("The value of y is: {y}");
}

Or by index:

fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);

    let five_hundred = x.0;

    let six_point_four = x.1;

    let one = x.2;
}

Empty tuple () is called unit and represents an empty value or return type.

Array Type

Fixed-length collection of homogeneous types, allocated on the stack:

fn main() {
    let a = [1, 2, 3, 4, 5];
}

Type annotation syntax:

let a: [i32; 5] = [1, 2, 3, 4, 5];

Initialize with repeated values:

let a = [3; 5]; // [3, 3, 3, 3, 3]

Use Vec<T> for dynamic arrays. Arrays are better when size is known at compile time.

Array Access
fn main() {
    let a = [1, 2, 3, 4, 5];

    let first = a[0];
    let second = a[1];
}

Out-of-bounds access causes runtime panic:

use std::io;

fn main() {
    let a = [1, 2, 3, 4, 5];

    println!("Please enter an array index.");

    let mut index = String::new();

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

    let index: usize = index
        .trim()
        .parse()
        .expect("Index entered was not a number");

    let element = a[index];

    println!("The value of the element at index {index} is: {element}");
}

Output:

thread 'main' panicked at src/main.rs:19:19:
index out of bounds: the len is 5 but the index is 10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Rust prevents buffer overflows by bounds checking at runtime.

Functions

Functions in Rust use snake_case convention and require explicit type annotations for parameters (unlike TypeScript’s optional inference).

Filename: src/main.rs

fn main() {
    println!("Hello, world!");

    another_function();
}

fn another_function() {
    println!("Another function.");
}

Functions are defined with fn and can be declared in any order within scope. The compiler doesn’t care about declaration order, similar to hoisting in JavaScript.

Parameters

Unlike TypeScript, parameter types are mandatory:

Filename: src/main.rs

fn main() {
    another_function(5);
}

fn another_function(x: i32) {
    println!("The value of x is: {x}");
}

Multiple parameters:

Filename: src/main.rs

fn main() {
    print_labeled_measurement(5, 'h');
}

fn print_labeled_measurement(value: i32, unit_label: char) {
    println!("The measurement is: {value}{unit_label}");
}

Statements vs Expressions

Critical distinction for Rust (unlike TypeScript where nearly everything is an expression):

  • Statements: Perform actions, return no value (let y = 6;)
  • Expressions: Evaluate to values (5 + 6, function calls, blocks)
fn main() {
    let y = 6;
}

This won’t compile (unlike JavaScript/TypeScript):

Filename: src/main.rs

fn main() {
    let x = (let y = 6);
}

Blocks are expressions:

Filename: src/main.rs

fn main() {
    let y = {
        let x = 3;
        x + 1
    };

    println!("The value of y is: {y}");
}

The block evaluates to 4 because x + 1 lacks a semicolon. Adding a semicolon converts an expression to a statement.

Return Values

Return type annotation required after ->. Functions return the last expression implicitly (no return keyword needed):

Filename: src/main.rs

fn five() -> i32 {
    5
}

fn main() {
    let x = five();

    println!("The value of x is: {x}");
}

Parameter and return example:

Filename: src/main.rs

fn main() {
    let x = plus_one(5);

    println!("The value of x is: {x}");
}

fn plus_one(x: i32) -> i32 {
    x + 1
}

Critical: Adding a semicolon to the return expression breaks the return:

Filename: src/main.rs

fn main() {
    let x = plus_one(5);

    println!("The value of x is: {x}");
}

fn plus_one(x: i32) -> i32 {
    x + 1;
}

This fails because statements return () (unit type), not the expected i32. The semicolon behavior is fundamentally different from TypeScript where semicolons are purely syntactic.

Comments

Rust uses // for line comments and /* */ for block comments:

// Line comment
/* Block comment */

Multi-line comments require // on each line:

// Multi-line comment
// continues here

Comments can be placed at line end or above code:

fn main() {
    let lucky_number = 7; // I'm feeling lucky today
}
fn main() {
    // I'm feeling lucky today
    let lucky_number = 7;
}

Documentation comments (/// and //!) are covered in Chapter 14.

Control Flow

if Expressions

if is an expression in Rust, not just a statement:

fn main() {
    let number = 3;

    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}

Conditions must be bool - no automatic type conversion:

fn main() {
    let number = 3;

    if number {
        println!("number was three");
    }
}

Error:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: mismatched types
 --> src/main.rs:4:8
  |
4 |     if number {
  |        ^^^^^^ expected `bool`, found integer

For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error

Explicit comparison required:

fn main() {
    let number = 3;

    if number != 0 {
        println!("number was something other than zero");
    }
}

Multiple Conditions

fn main() {
    let number = 6;

    if number % 4 == 0 {
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        println!("number is divisible by 3");
    } else if number % 2 == 0 {
        println!("number is divisible by 2");
    } else {
        println!("number is not divisible by 4, 3, or 2");
    }
}

if in let Statements

Since if is an expression, it can assign values:

fn main() {
    let condition = true;
    let number = if condition { 5 } else { 6 };

    println!("The value of number is: {number}");
}

All branches must return the same type:

fn main() {
    let condition = true;

    let number = if condition { 5 } else { "six" };

    println!("The value of number is: {number}");
}

Error:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: `if` and `else` have incompatible types
 --> src/main.rs:4:44
  |
4 |     let number = if condition { 5 } else { "six" };
  |                                 -          ^^^^^ expected integer, found `&str`
  |                                 |
  |                                 expected because of this

For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error

Loops

loop

Infinite loop until explicit break:

fn main() {
    loop {
        println!("again!");
    }
}

Return values from loops:

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {result}");
}

Loop Labels

Label loops for nested control:

fn main() {
    let mut count = 0;
    'counting_up: loop {
        println!("count = {count}");
        let mut remaining = 10;

        loop {
            println!("remaining = {remaining}");
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up;
            }
            remaining -= 1;
        }

        count += 1;
    }
    println!("End count = {count}");
}

Output:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.58s
     Running `target/debug/loops`
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2

while

Conditional loops:

fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{number}!");

        number -= 1;
    }

    println!("LIFTOFF!!!");
}

for

Preferred for collection iteration. Compare while approach:

fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;

    while index < 5 {
        println!("the value is: {}", a[index]);

        index += 1;
    }
}

With safer for loop:

fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a {
        println!("the value is: {element}");
    }
}

Using ranges:

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

for loops are preferred in Rust for safety and performance - no bounds checking overhead and elimination of index errors.

Understanding Ownership

Ownership is Rust’s core memory management system that enables memory safety without garbage collection. This chapter covers ownership, borrowing, slices, and memory layout—concepts that differentiate Rust from garbage-collected languages like JavaScript/TypeScript.

What Is Ownership?

Ownership is Rust’s memory management system with compile-time rules that prevent memory safety issues without runtime overhead. Unlike garbage-collected languages, Rust determines memory cleanup at compile time.

Stack vs Heap

Stack: LIFO structure for fixed-size data. Fast allocation/deallocation, automatic cleanup. Heap: Runtime allocation for dynamic-size data. Returns pointer, requires explicit management.

Stack access is faster due to locality. Heap access requires pointer dereferencing. Ownership primarily manages heap data.

Ownership Rules

  • Each value has exactly one owner
  • Only one owner at a time
  • Value is dropped when owner goes out of scope

Variable Scope

let s = "hello";

String literals are immutable and stack-allocated. The variable s is valid from declaration until scope end.

fn main() {
    {                      // s is not valid here, since it's not yet declared
        let s = "hello";   // s is valid from this point forward

        // do stuff with s
    }                      // this scope is now over, and s is no longer valid
}

The String Type

String manages heap-allocated, mutable text data:

let s = String::from("hello");

Unlike string literals, String can be mutated and has dynamic size:

fn main() {
    let mut s = String::from("hello");

    s.push_str(", world!"); // push_str() appends a literal to a String

    println!("{s}"); // this will print `hello, world!`
}

Memory and Allocation

String requires heap allocation for dynamic content. Rust automatically calls drop when the owner goes out of scope—similar to RAII in C++.

fn main() {
    {
        let s = String::from("hello"); // s is valid from this point forward

        // do stuff with s
    }                                  // this scope is now over, and s is no
                                       // longer valid
}

Variables and Data Interacting with Move

Simple stack values are copied:

fn main() {
    let x = 5;
    let y = x;
}

Heap-allocated values are moved:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
}

String contains three parts on the stack: pointer, length, capacity. Assignment copies these metadata but not heap data. Rust invalidates the original variable to prevent double-free errors.

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;

    println!("{s1}, world!");
}

Error: value used after move. This prevents the double-free memory safety issue.

Scope and Assignment

Assigning a new value immediately drops the previous value:

fn main() {
    let mut s = String::from("hello");
    s = String::from("ahoy");

    println!("{s}, world!");
}

Variables and Data Interacting with Clone

For explicit deep copying:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();

    println!("s1 = {s1}, s2 = {s2}");
}

clone() is expensive—it copies heap data.

Stack-Only Data: Copy

Types implementing Copy trait are duplicated instead of moved:

fn main() {
    let x = 5;
    let y = x;

    println!("x = {x}, y = {y}");
}

Copy types include:

  • Integer types (u32, i32, etc.)
  • Boolean (bool)
  • Floating-point types (f64)
  • Character (char)
  • Tuples of Copy types

Types with Drop trait cannot implement Copy.

Ownership and Functions

Function calls behave like assignment—values are moved or copied:

fn main() {
    let s = String::from("hello");  // s comes into scope

    takes_ownership(s);             // s's value moves into the function...
                                    // ... and so is no longer valid here

    let x = 5;                      // x comes into scope

    makes_copy(x);                  // Because i32 implements the Copy trait,
                                    // x does NOT move into the function,
                                    // so it's okay to use x afterward.

} // Here, x goes out of scope, then s. However, because s's value was moved,
  // nothing special happens.

fn takes_ownership(some_string: String) { // some_string comes into scope
    println!("{some_string}");
} // Here, some_string goes out of scope and `drop` is called. The backing
  // memory is freed.

fn makes_copy(some_integer: i32) { // some_integer comes into scope
    println!("{some_integer}");
} // Here, some_integer goes out of scope. Nothing special happens.

Return Values and Scope

Functions can transfer ownership via return values:

fn main() {
    let s1 = gives_ownership();        // gives_ownership moves its return
                                       // value into s1

    let s2 = String::from("hello");    // s2 comes into scope

    let s3 = takes_and_gives_back(s2); // s2 is moved into
                                       // takes_and_gives_back, which also
                                       // moves its return value into s3
} // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing
  // happens. s1 goes out of scope and is dropped.

fn gives_ownership() -> String {       // gives_ownership will move its
                                       // return value into the function
                                       // that calls it

    let some_string = String::from("yours"); // some_string comes into scope

    some_string                        // some_string is returned and
                                       // moves out to the calling
                                       // function
}

// This function takes a String and returns a String.
fn takes_and_gives_back(a_string: String) -> String {
    // a_string comes into
    // scope

    a_string  // a_string is returned and moves out to the calling function
}

Returning ownership for every function parameter is verbose. Rust provides references for this use case:

fn main() {
    let s1 = String::from("hello");

    let (s2, len) = calculate_length(s1);

    println!("The length of '{s2}' is {len}.");
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len(); // len() returns the length of a String

    (s, length)
}

References and Borrowing

References allow using values without taking ownership. A reference is guaranteed to point to a valid value for its lifetime.

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{s1}' is {len}.");
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

The & syntax creates a reference. &s1 refers to s1 without owning it. When the reference goes out of scope, the value isn’t dropped because the reference doesn’t own it.

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{s1}' is {len}.");
}

fn calculate_length(s: &String) -> usize { // s is a reference to a String
    s.len()
} // Here, s goes out of scope. But because s does not have ownership of what
  // it refers to, the String is not dropped.

Creating a reference is called borrowing. References are immutable by default:

fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}

Mutable References

Use &mut for mutable references:

fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

Borrowing rules:

  • Only one mutable reference per value at a time
  • Cannot mix mutable and immutable references in overlapping scopes

This prevents data races at compile time:

fn main() {
    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s;

    println!("{r1}, {r2}");
}

Multiple mutable references are allowed in separate scopes:

fn main() {
    let mut s = String::from("hello");

    {
        let r1 = &mut s;
    } // r1 goes out of scope here, so we can make a new reference with no problems.

    let r2 = &mut s;
}

Cannot combine mutable and immutable references:

fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    let r3 = &mut s; // BIG PROBLEM

    println!("{r1}, {r2}, and {r3}");
}

Reference scopes end at their last usage, not at the end of the block:

fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    println!("{r1} and {r2}");
    // Variables r1 and r2 will not be used after this point.

    let r3 = &mut s; // no problem
    println!("{r3}");
}

Dangling References

Rust prevents dangling references through compile-time checks:

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");

    &s
}

Error: function returns a borrowed value with no value to borrow from.

Solution—return the value directly:

fn main() {
    let string = no_dangle();
}

fn no_dangle() -> String {
    let s = String::from("hello");

    s
}

The Rules of References

  • At any time: either one mutable reference OR any number of immutable references
  • References must always be valid

The Slice Type

Slices reference contiguous sequences of elements in collections without taking ownership.

Problem: Write a function returning the first word from a space-separated string.

Without slices, we might return an index:

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {}

Issues with index-based approach:

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s); // word will get the value 5

    s.clear(); // this empties the String, making it equal to ""

    // word still has the value 5 here, but s no longer has any content that we
    // could meaningfully use with the value 5, so word is now totally invalid!
}

The index becomes invalid after s.clear(), but the compiler can’t catch this error.

String Slices

A string slice (&str) references a portion of a String:

fn main() {
    let s = String::from("hello world");

    let hello = &s[0..5];
    let world = &s[6..11];
}

Slice syntax: &s[start..end] where end is exclusive.

Shortcuts:

let s = String::from("hello");

let slice = &s[0..2];
let slice = &s[..2];    // Same as above

let slice = &s[3..len];
let slice = &s[3..];    // Same as above

let slice = &s[0..len];
let slice = &s[..];     // Entire string

Improved first_word using slices:

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {}

Slices prevent the previous bug by enforcing borrowing rules:

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s);

    s.clear(); // error!

    println!("the first word is: {word}");
}

Compile error: cannot borrow as mutable while immutable reference exists.

String Literals as Slices

String literals are &str types—slices pointing to binary data:

let s = "Hello, world!"; // s is &str

String Slices as Parameters

Better function signature accepts both &String and &str:

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {
    let my_string = String::from("hello world");

    // `first_word` works on slices of `String`s, whether partial or whole.
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // `first_word` also works on references to `String`s, which are equivalent
    // to whole slices of `String`s.
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // `first_word` works on slices of string literals, whether partial or
    // whole.
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word = first_word(my_string_literal);
}

Usage:

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {
    let my_string = String::from("hello world");

    // `first_word` works on slices of `String`s, whether partial or whole.
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // `first_word` also works on references to `String`s, which are equivalent
    // to whole slices of `String`s.
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // `first_word` works on slices of string literals, whether partial or
    // whole.
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word = first_word(my_string_literal);
}

Other Slices

Array slices work similarly:

let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];   // Type: &[i32]
assert_eq!(slice, &[2, 3]);

Summary

Ownership, borrowing, and slices provide compile-time memory safety without runtime overhead. These concepts affect many other Rust features throughout the language.

Using Structs to Structure Related Data

Structs are custom data types that group related values with named fields. Unlike tuples, structs provide semantic meaning through field names and don’t rely on positional access.

This chapter covers:

  • Struct definition and instantiation syntax
  • Associated functions and methods
  • Ownership considerations with struct data

Structs and enums form the foundation of Rust’s type system, enabling compile-time guarantees and zero-cost abstractions.

Defining and Instantiating Structs

Structs provide named fields for data grouping, offering better semantics than tuples when field order shouldn’t matter.

Basic Syntax

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {}

Instantiation uses key-value pairs in any order:

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    let user1 = User {
        active: true,
        username: String::from("someusername123"),
        email: String::from("someone@example.com"),
        sign_in_count: 1,
    };
}

Access and mutation require the entire instance to be mutable:

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    let mut user1 = User {
        active: true,
        username: String::from("someusername123"),
        email: String::from("someone@example.com"),
        sign_in_count: 1,
    };

    user1.email = String::from("anotheremail@example.com");
}

Functions can return struct instances as the last expression:

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        username: username,
        email: email,
        sign_in_count: 1,
    }
}

fn main() {
    let user1 = build_user(
        String::from("someone@example.com"),
        String::from("someusername123"),
    );
}

Field Init Shorthand

When parameter names match field names, use shorthand syntax:

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        username,
        email,
        sign_in_count: 1,
    }
}

fn main() {
    let user1 = build_user(
        String::from("someone@example.com"),
        String::from("someusername123"),
    );
}

Struct Update Syntax

Create instances from existing ones with selective field updates:

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    // --snip--

    let user1 = User {
        email: String::from("someone@example.com"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };

    let user2 = User {
        active: user1.active,
        username: user1.username,
        email: String::from("another@example.com"),
        sign_in_count: user1.sign_in_count,
    };
}

The .. operator copies remaining fields:

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    // --snip--

    let user1 = User {
        email: String::from("someone@example.com"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };

    let user2 = User {
        email: String::from("another@example.com"),
        ..user1
    };
}

Ownership Note: Struct update uses move semantics. If any moved field contains non-Copy data (like String), the original instance becomes unusable. Fields implementing Copy (like primitives) are copied, not moved.

Tuple Structs

Tuple structs provide struct semantics with positional fields:

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);
}

Different tuple struct types are incompatible even with identical field types. Access uses dot notation with indices: origin.0, origin.1, etc.

Unit-Like Structs

Structs without fields are useful for trait implementations without data:

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

Ownership in Structs

Structs typically own their data using owned types like String rather than references like &str. References in structs require lifetime parameters:

struct User {
    active: bool,
    username: &str,    // Error: missing lifetime specifier
    email: &str,       // Error: missing lifetime specifier
    sign_in_count: u64,
}

fn main() {
    let user1 = User {
        active: true,
        username: "someusername123",
        email: "someone@example.com",
        sign_in_count: 1,
    };
}

Compiler output:

$ cargo run
   Compiling structs v0.1.0 (file:///projects/structs)
error[E0106]: missing lifetime specifier
 --> src/main.rs:3:15
  |
3 |     username: &str,
  |               ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct User<'a> {
2 |     active: bool,
3 ~     username: &'a str,
  |

error[E0106]: missing lifetime specifier
 --> src/main.rs:4:12
  |
4 |     email: &str,
  |            ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct User<'a> {
2 |     active: bool,
3 |     username: &str,
4 ~     email: &'a str,
  |

For more information about this error, try `rustc --explain E0106`.
error: could not compile `structs` (bin "structs") due to 2 previous errors

Lifetimes are covered in Chapter 10. For now, use owned types to avoid complexity.

An Example Program Using Structs

This example demonstrates the progression from loose parameters to structured data using a rectangle area calculator.

Initial Implementation

fn main() {
    let width1 = 30;
    let height1 = 50;

    println!(
        "The area of the rectangle is {} square pixels.",
        area(width1, height1)
    );
}

fn area(width: u32, height: u32) -> u32 {
    width * height
}

Output:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
     Running `target/debug/rectangles`
The area of the rectangle is 1500 square pixels.

The function signature fn area(width: u32, height: u32) -> u32 doesn’t express the relationship between parameters. This creates maintenance issues and potential parameter ordering mistakes.

Refactoring with Tuples

fn main() {
    let rect1 = (30, 50);

    println!(
        "The area of the rectangle is {} square pixels.",
        area(rect1)
    );
}

fn area(dimensions: (u32, u32)) -> u32 {
    dimensions.0 * dimensions.1
}

Tuples group related data but lose semantic meaning. Index-based access (dimensions.0, dimensions.1) reduces code clarity and introduces potential errors.

Refactoring with Structs

Structs provide both grouping and semantic meaning:

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        area(&rect1)
    );
}

fn area(rectangle: &Rectangle) -> u32 {
    rectangle.width * rectangle.height
}

Benefits:

  • Clear function signature expressing intent
  • Named field access (rectangle.width, rectangle.height)
  • Borrowing instead of ownership transfer
  • Type safety preventing parameter confusion

Debug Output

Structs don’t implement Display by default. Use Debug trait for development output:

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {rect1}");
}

Error:

error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`

Enable debug output with #[derive(Debug)]:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {rect1:?}");
}

Output:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/rectangles`
rect1 is Rectangle { width: 30, height: 50 }

Pretty-print with {:#?}:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/rectangles`
rect1 is Rectangle {
    width: 30,
    height: 50,
}

Debug Macro

The dbg! macro provides file/line information and returns ownership:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let scale = 2;
    let rect1 = Rectangle {
        width: dbg!(30 * scale),
        height: 50,
    };

    dbg!(&rect1);
}

Output:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
     Running `target/debug/rectangles`
[src/main.rs:10:16] 30 * scale = 60
[src/main.rs:14:5] &rect1 = Rectangle {
    width: 60,
    height: 50,
}

Key Differences:

  • println! takes references, outputs to stdout
  • dbg! takes ownership (returns it), outputs to stderr with location info

Other derivable traits are listed in Appendix C. Custom trait implementation is covered in Chapter 10.

Method Syntax

Methods are functions defined within a struct’s context using impl blocks. The first parameter is always self, representing the instance being called.

Defining Methods

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}

Key Points:

  • impl Rectangle creates an implementation block
  • &self is shorthand for self: &Self where Self aliases the impl type
  • Methods use dot notation: rect1.area()
  • Borrowing rules apply: &self (immutable), &mut self (mutable), self (take ownership)

Using &self preserves ownership in the caller, similar to function parameters. Methods consuming self are rare and typically used for transformations.

Methods vs Fields

Methods can have the same name as fields:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn width(&self) -> bool {
        self.width > 0
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    if rect1.width() {
        println!("The rectangle has a nonzero width; it is {}", rect1.width);
    }
}
  • rect1.width() calls the method
  • rect1.width accesses the field

This pattern enables getter methods for controlled field access, supporting public methods with private fields.

Automatic Referencing

Rust automatically handles referencing/dereferencing for method calls:

#[derive(Debug,Copy,Clone)]
struct Point {
    x: f64,
    y: f64,
}

impl Point {
   fn distance(&self, other: &Point) -> f64 {
       let x_squared = f64::powi(other.x - self.x, 2);
       let y_squared = f64::powi(other.y - self.y, 2);

       f64::sqrt(x_squared + y_squared)
   }
}
let p1 = Point { x: 0.0, y: 0.0 };
let p2 = Point { x: 5.0, y: 6.5 };
p1.distance(&p2);
(&p1).distance(&p2);  // Equivalent

This eliminates the need for manual * or -> operators found in C/C++.

Methods with Parameters

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

Expected output:

Can rect1 hold rect2? true
Can rect1 hold rect3? false

Implementation:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

Parameters after self work like regular function parameters. Use borrowing for parameters you don’t need to own.

Associated Functions

Functions in impl blocks without self are associated functions (similar to static methods):

Filename: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn square(size: u32) -> Self {
        Self {
            width: size,
            height: size,
        }
    }
}

fn main() {
    let sq = Rectangle::square(3);
}

Called using :: syntax: Rectangle::square(3). Common for constructors like String::from().

Multiple impl Blocks

Structs can have multiple impl blocks:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

Multiple blocks are useful with generics and traits (Chapter 10).

Summary

Structs provide custom types with named fields and associated behavior through methods. Key concepts:

  • Field access: Dot notation with borrowing considerations
  • Methods: Functions with self parameter for instance behavior
  • Associated functions: Type-scoped functions for construction/utilities
  • impl blocks: Organize type-related functionality
  • Automatic referencing: Eliminates manual pointer dereferencing

Next: Enums provide another approach to custom types with variant-based data modeling.

Enums and Pattern Matching

Enums define types by enumerating possible variants, enabling type-safe variant handling through pattern matching. This chapter covers:

  • Enum definition and variant syntax
  • The Option<T> enum as Rust’s null safety mechanism
  • match expressions for exhaustive pattern matching
  • if let and let else for concise single-pattern matching

Enums combined with pattern matching provide powerful abstractions for modeling domain data and eliminating entire classes of runtime errors.

Defining an Enum

Enums model data that can be one of several distinct variants. Each variant can carry different types and amounts of data.

Basic Syntax

enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    route(IpAddrKind::V4);
    route(IpAddrKind::V6);
}

fn route(ip_kind: IpAddrKind) {}

Enum values are created using namespace syntax:

enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    route(IpAddrKind::V4);
    route(IpAddrKind::V6);
}

fn route(ip_kind: IpAddrKind) {}

Both values have type IpAddrKind, enabling uniform function parameters:

enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    route(IpAddrKind::V4);
    route(IpAddrKind::V6);
}

fn route(ip_kind: IpAddrKind) {}

Data in Variants

Variants can carry associated data directly:

fn main() {
    enum IpAddr {
        V4(String),
        V6(String),
    }

    let home = IpAddr::V4(String::from("127.0.0.1"));

    let loopback = IpAddr::V6(String::from("::1"));
}

Variant names become constructor functions: IpAddr::V4() takes a String and returns an IpAddr.

Each variant can have different data types:

fn main() {
    enum IpAddr {
        V4(u8, u8, u8, u8),
        V6(String),
    }

    let home = IpAddr::V4(127, 0, 0, 1);

    let loopback = IpAddr::V6(String::from("::1"));
}

The standard library’s IpAddr uses structs within variants:

struct Ipv4Addr {
    // --snip--
}

struct Ipv6Addr {
    // --snip--
}

enum IpAddr {
    V4(Ipv4Addr),
    V6(Ipv6Addr),
}

Complex Variants

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {}

This enum replaces what would require multiple struct types:

struct QuitMessage; // unit struct
struct MoveMessage {
    x: i32,
    y: i32,
}
struct WriteMessage(String); // tuple struct
struct ChangeColorMessage(i32, i32, i32); // tuple struct

fn main() {}

Enums enable functions accepting any variant as a single type parameter, unlike separate structs.

Methods on Enums

Enums support methods via impl blocks:

fn main() {
    enum Message {
        Quit,
        Move { x: i32, y: i32 },
        Write(String),
        ChangeColor(i32, i32, i32),
    }

    impl Message {
        fn call(&self) {
            // method body would be defined here
        }
    }

    let m = Message::Write(String::from("hello"));
    m.call();
}

The Option<T> Enum

Option<T> encodes nullable values safely in the type system:

enum Option<T> {
    None,
    Some(T),
}

Option<T> is included in the prelude. Variants Some and None are directly accessible without the Option:: prefix.

Examples:

fn main() {
    let some_number = Some(5);
    let some_char = Some('e');

    let absent_number: Option<i32> = None;
}

Option vs Null Safety

Unlike TypeScript’s null and undefined, Rust prevents direct operations on Option<T> values:

fn main() {
    let x: i8 = 5;
    let y: Option<i8> = Some(5);

    let sum = x + y;
}

Compiler error:

$ cargo run
   Compiling enums v0.1.0 (file:///projects/enums)
error[E0277]: cannot add `Option<i8>` to `i8`
 --> src/main.rs:5:17
  |
5 |     let sum = x + y;
  |                 ^ no implementation for `i8 + Option<i8>`
  |
  = help: the trait `Add<Option<i8>>` is not implemented for `i8`
  = help: the following other types implement trait `Add<Rhs>`:
            `&i8` implements `Add<i8>`
            `&i8` implements `Add`
            `i8` implements `Add<&i8>`
            `i8` implements `Add`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `enums` (bin "enums") due to 1 previous error

This compile-time check eliminates null pointer exceptions. You must explicitly handle both Some(T) and None cases before accessing the inner value.

Key Benefits:

  • Explicit opt-in for nullable types via Option<T>
  • Compile-time guarantee that non-Option types are never null
  • Forces explicit null checking at usage sites

To extract values from Option<T>, use pattern matching (match) or Option<T> methods. Pattern matching provides exhaustive case handling and value extraction from variants.

The match Control Flow Construct

match enables pattern matching against enum variants with compile-time exhaustiveness checking. Each pattern can extract data from variants and execute corresponding code.

Basic Syntax

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

fn main() {}

Structure:

  • match keyword followed by expression to match
  • Arms with pattern => code syntax
  • Arms evaluated in order until match found
  • Matching arm’s code becomes the expression’s return value

For multi-line arm code, use curly braces:

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

fn main() {}

Pattern Binding

Match arms can bind to data within enum variants:

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn main() {}

Extract data using variable binding in patterns:

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State quarter from {state:?}!");
            25
        }
    }
}

fn main() {
    value_in_cents(Coin::Quarter(UsState::Alaska));
}

The state variable binds to the inner value from Coin::Quarter(UsState::Alaska), making it accessible within the arm.

Matching Option<T>

Common pattern for safe null handling:

fn main() {
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}

Execution flow:

  • Some(5) matches Some(i), binding i = 5
  • None matches None pattern directly
  • Each variant must be handled explicitly

Exhaustiveness

Match expressions must handle all possible variants:

fn main() {
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}

Compiler error:

$ cargo run
   Compiling enums v0.1.0 (file:///projects/enums)
error[E0004]: non-exhaustive patterns: `None` not covered
 --> src/main.rs:3:15
  |
3 |         match x {
  |               ^ pattern `None` not covered
  |
note: `Option<i32>` defined here
 --> /rustc/4eb161250e340c8f48f66e2b929ef4a5bed7c181/library/core/src/option.rs:572:1
 ::: /rustc/4eb161250e340c8f48f66e2b929ef4a5bed7c181/library/core/src/option.rs:576:5
  |
  = note: not covered
  = note: the matched value is of type `Option<i32>`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
  |
4 ~             Some(i) => Some(i + 1),
5 ~             None => todo!(),
  |

For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums` (bin "enums") due to 1 previous error

Exhaustiveness prevents runtime errors from unhandled cases, especially critical for Option<T> null safety.

Catch-All Patterns

Handle specific values with catch-all for remaining cases:

fn main() {
    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        other => move_player(other),
    }

    fn add_fancy_hat() {}
    fn remove_fancy_hat() {}
    fn move_player(num_spaces: u8) {}
}

Use _ placeholder when catch-all value isn’t needed:

fn main() {
    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        _ => reroll(),
    }

    fn add_fancy_hat() {}
    fn remove_fancy_hat() {}
    fn reroll() {}
}

Use unit value () for no-op catch-all:

fn main() {
    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        _ => (),
    }

    fn add_fancy_hat() {}
    fn remove_fancy_hat() {}
}

Key Points:

  • Catch-all patterns must be last (evaluated in order)
  • _ ignores the value without binding
  • () performs no operation
  • Catch-all ensures exhaustiveness for large value spaces

Concise Control Flow with if let and let else

if let provides concise pattern matching for single patterns, trading exhaustiveness checking for brevity.

Basic if let Syntax

Instead of verbose match for single-pattern cases:

fn main() {
    let config_max = Some(3u8);
    match config_max {
        Some(max) => println!("The maximum is configured to be {max}"),
        _ => (),
    }
}

Use if let for cleaner code:

fn main() {
    let config_max = Some(3u8);
    if let Some(max) = config_max {
        println!("The maximum is configured to be {max}");
    }
}

Syntax: if let pattern = expression { body }

  • Pattern matching works identically to match
  • Only executes when pattern matches
  • Loses exhaustiveness checking

if let with else

Handle non-matching cases with else:

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn main() {
    let coin = Coin::Penny;
    let mut count = 0;
    match coin {
        Coin::Quarter(state) => println!("State quarter from {state:?}!"),
        _ => count += 1,
    }
}

Equivalent using if let:

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn main() {
    let coin = Coin::Penny;
    let mut count = 0;
    if let Coin::Quarter(state) = coin {
        println!("State quarter from {state:?}!");
    } else {
        count += 1;
    }
}

let...else for Early Returns

let...else binds values or returns early, maintaining linear control flow:

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

impl UsState {
    fn existed_in(&self, year: u16) -> bool {
        match self {
            UsState::Alabama => year >= 1819,
            UsState::Alaska => year >= 1959,
            // -- snip --
        }
    }
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn describe_state_quarter(coin: Coin) -> Option<String> {
    if let Coin::Quarter(state) = coin {
        if state.existed_in(1900) {
            Some(format!("{state:?} is pretty old, for America!"))
        } else {
            Some(format!("{state:?} is relatively new."))
        }
    } else {
        None
    }
}

fn main() {
    if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
        println!("{desc}");
    }
}
#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

impl UsState {
    fn existed_in(&self, year: u16) -> bool {
        match self {
            UsState::Alabama => year >= 1819,
            UsState::Alaska => year >= 1959,
            // -- snip --
        }
    }
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn describe_state_quarter(coin: Coin) -> Option<String> {
    let state = if let Coin::Quarter(state) = coin {
        state
    } else {
        return None;
    };

    if state.existed_in(1900) {
        Some(format!("{state:?} is pretty old, for America!"))
    } else {
        Some(format!("{state:?} is relatively new."))
    }
}

fn main() {
    if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
        println!("{desc}");
    }
}

Cleaner with let...else:

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

impl UsState {
    fn existed_in(&self, year: u16) -> bool {
        match self {
            UsState::Alabama => year >= 1819,
            UsState::Alaska => year >= 1959,
            // -- snip --
        }
    }
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn describe_state_quarter(coin: Coin) -> Option<String> {
    let Coin::Quarter(state) = coin else {
        return None;
    };

    if state.existed_in(1900) {
        Some(format!("{state:?} is pretty old, for America!"))
    } else {
        Some(format!("{state:?} is relatively new."))
    }
}

fn main() {
    if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
        println!("{desc}");
    }
}

let...else Properties:

  • Pattern matches: binds value in outer scope and continues
  • Pattern fails: executes else block, which must diverge (return, panic, etc.)
  • Maintains “happy path” flow without nested conditionals

When to Use Each

PatternUse CaseTrade-offs
matchMultiple patterns, exhaustiveness requiredVerbose for single patterns
if letSingle pattern, optional else caseNo exhaustiveness checking
let...elseExtract value or early returnMust diverge on mismatch

Summary

Enums with pattern matching provide type-safe variant modeling:

  • Enums: Model exclusive variants with associated data
  • Option<T>: Eliminates null pointer errors through type safety
  • match: Exhaustive pattern matching with data extraction
  • if let/let...else: Concise syntax for common patterns

These constructs enable robust APIs where invalid states are unrepresentable, moving error handling from runtime to compile time.

Managing Growing Projects with Packages, Crates, and Modules

Rust’s module system provides tools for organizing code, controlling scope, and managing visibility—essential for building maintainable applications. Unlike JavaScript/TypeScript’s file-based modules, Rust uses an explicit module hierarchy with compile-time visibility controls.

Core Concepts

Rust’s module system consists of:

  • Packages: Cargo’s build units containing one or more crates
  • Crates: Compilation units producing libraries or executables
  • Modules: Organizational units controlling scope and privacy
  • Paths: Item addressing within the module tree

Key Differences from JavaScript/TypeScript

Unlike ES modules where each file is implicitly a module, Rust modules are explicitly declared and can span multiple files or exist inline. Privacy is explicit—items are private by default, unlike JavaScript’s implicit public exports.

The module system enforces encapsulation at compile time, preventing access to private implementation details that could be accessed through reflection or direct property access in JavaScript.

Packages and Crates

Crates

A crate is Rust’s compilation unit—the smallest code unit the compiler processes. Crates come in two forms:

  • Binary crates: Executables with a main function (like Node.js applications)
  • Library crates: Reusable code without main (like npm packages)

The crate root is the source file where compilation begins, forming the root module of the crate’s module tree.

Packages

A package contains one or more crates plus a Cargo.toml manifest. Think of it as similar to a Node.js project with package.json.

Package rules:

  • Must contain at least one crate
  • Can contain at most one library crate
  • Can contain unlimited binary crates

Cargo Conventions

$ cargo new my-project
     Created binary (application) `my-project` package

Cargo follows these conventions:

  • src/main.rs → binary crate root (package name)
  • src/lib.rs → library crate root (package name)
  • src/bin/ → additional binary crates (one per file)

A package with both src/main.rs and src/lib.rs contains two crates: a binary and library, both named after the package. This pattern is common for CLI tools that expose both an executable and library API.

Defining Modules to Control Scope and Privacy

Module System Quick Reference

  • Crate root: Compiler starts at src/lib.rs (library) or src/main.rs (binary)
  • Module declaration: mod garden; looks for code in:
    • Inline: mod garden { ... }
    • File: src/garden.rs
    • Directory: src/garden/mod.rs
  • Submodules: Declared in parent module files, found in subdirectories
  • Paths: Access items via crate::module::item (absolute) or module::item (relative)
  • Privacy: Items are private by default; use pub to expose them
  • use keyword: Creates shortcuts to reduce path repetition

Example Structure

backyard
├── Cargo.toml
└── src
    ├── garden
    │   └── vegetables.rs
    ├── garden.rs
    └── main.rs
use crate::garden::vegetables::Asparagus;

pub mod garden;

fn main() {
    let plant = Asparagus {};
    println!("I'm growing {plant:?}!");
}
pub mod vegetables;
#[derive(Debug)]
pub struct Asparagus {}

Organizing Code with Modules

Modules provide namespace organization and privacy control. Unlike JavaScript imports which expose everything explicitly exported, Rust items are private by default.

mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}

        fn seat_at_table() {}
    }

    mod serving {
        fn take_order() {}

        fn serve_order() {}

        fn take_payment() {}
    }
}

This creates a module tree:

crate
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment

The crate root forms an implicit crate module. Child modules can access parent items, but parents cannot access private child items without explicit pub declarations.

Paths for Referring to an Item in the Module Tree

Paths specify item locations within the module tree using :: syntax:

  • Absolute paths: Start from crate root with crate:: (like absolute file paths)
  • Relative paths: Start from current module using self, super, or module names
mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();
}

This code fails to compile because hosting module is private:

$ cargo build
   Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: module `hosting` is private
 --> src/lib.rs:9:28
  |
9 |     crate::front_of_house::hosting::add_to_waitlist();
  |                            ^^^^^^^  --------------- function `add_to_waitlist` is not publicly re-exported
  |                            |
  |                            private module
  |
note: the module `hosting` is defined here
 --> src/lib.rs:2:5
  |
2 |     mod hosting {
  |     ^^^^^^^^^^^

error[E0603]: module `hosting` is private
  --> src/lib.rs:12:21
   |
12 |     front_of_house::hosting::add_to_waitlist();
   |                     ^^^^^^^  --------------- function `add_to_waitlist` is not publicly re-exported
   |                     |
   |                     private module
   |
note: the module `hosting` is defined here
  --> src/lib.rs:2:5
   |
2  |     mod hosting {
   |     ^^^^^^^^^^^

For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` (lib) due to 2 previous errors

Privacy Rules

Rust’s privacy model differs from JavaScript/TypeScript:

  • Default private: All items are private to parent modules by default
  • Child access: Child modules can access ancestor module items
  • Parent restriction: Parent modules cannot access private child items

This enforces encapsulation—internal implementation details remain hidden unless explicitly exposed.

Exposing Paths with the pub Keyword

Making the hosting module public isn’t sufficient:

mod front_of_house {
    pub mod hosting {
        fn add_to_waitlist() {}
    }
}

// -- snip --
pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();
}
$ cargo build
   Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: function `add_to_waitlist` is private
  --> src/lib.rs:10:37
   |
10 |     crate::front_of_house::hosting::add_to_waitlist();
   |                                     ^^^^^^^^^^^^^^^ private function
   |
note: the function `add_to_waitlist` is defined here
  --> src/lib.rs:3:9
   |
3  |         fn add_to_waitlist() {}
   |         ^^^^^^^^^^^^^^^^^^^^

error[E0603]: function `add_to_waitlist` is private
  --> src/lib.rs:13:30
   |
13 |     front_of_house::hosting::add_to_waitlist();
   |                              ^^^^^^^^^^^^^^^ private function
   |
note: the function `add_to_waitlist` is defined here
  --> src/lib.rs:3:9
   |
3  |         fn add_to_waitlist() {}
   |         ^^^^^^^^^^^^^^^^^^^^

For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` (lib) due to 2 previous errors

The pub keyword on a module only allows access to the module itself, not its contents. Both the module and the function must be public:

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

// -- snip --
pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();
}

Starting Relative Paths with super

Use super to access parent module items (like ../ in file paths):

fn deliver_order() {}

mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();
        super::deliver_order();
    }

    fn cook_order() {}
}

Making Structs and Enums Public

Structs: Fields remain private by default even when struct is public

mod back_of_house {
    pub struct Breakfast {
        pub toast: String,
        seasonal_fruit: String,
    }

    impl Breakfast {
        pub fn summer(toast: &str) -> Breakfast {
            Breakfast {
                toast: String::from(toast),
                seasonal_fruit: String::from("peaches"),
            }
        }
    }
}

pub fn eat_at_restaurant() {
    // Order a breakfast in the summer with Rye toast.
    let mut meal = back_of_house::Breakfast::summer("Rye");
    // Change our mind about what bread we'd like.
    meal.toast = String::from("Wheat");
    println!("I'd like {} toast please", meal.toast);

    // The next line won't compile if we uncomment it; we're not allowed
    // to see or modify the seasonal fruit that comes with the meal.
    // meal.seasonal_fruit = String::from("blueberries");
}

Structs with private fields require constructor functions since you cannot directly instantiate them.

Enums: All variants become public when enum is public

mod back_of_house {
    pub enum Appetizer {
        Soup,
        Salad,
    }
}

pub fn eat_at_restaurant() {
    let order1 = back_of_house::Appetizer::Soup;
    let order2 = back_of_house::Appetizer::Salad;
}

Bringing Paths into Scope with the use Keyword

The use keyword creates shortcuts to paths, reducing repetition. Think of it as creating local aliases, similar to import statements in JavaScript/TypeScript.

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
}

The use statement creates a shortcut valid only within its scope:

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::hosting;

mod customer {
    pub fn eat_at_restaurant() {
        hosting::add_to_waitlist();
    }
}

Idiomatic use Patterns

Functions: Import the parent module, not the function directly

// Idiomatic
use crate::front_of_house::hosting;
hosting::add_to_waitlist();

// Less clear where function is defined
use crate::front_of_house::hosting::add_to_waitlist;
add_to_waitlist();

Structs/Enums/Types: Import the full path

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert(1, 2);
}

Name conflicts: Use parent modules to disambiguate

use std::fmt;
use std::io;

fn function1() -> fmt::Result {
    // --snip--
    Ok(())
}

fn function2() -> io::Result<()> {
    // --snip--
    Ok(())
}

Providing New Names with the as Keyword

Create aliases to resolve naming conflicts:

use std::fmt::Result;
use std::io::Result as IoResult;

fn function1() -> Result {
    // --snip--
    Ok(())
}

fn function2() -> IoResult<()> {
    // --snip--
    Ok(())
}

Re-exporting Names with pub use

Make imported items available to external code (similar to TypeScript’s export { ... } from ...):

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
}

Re-exporting allows exposing a different public API structure than your internal organization.

Using External Packages

Add dependencies to Cargo.toml:

rand = "0.8.5"

Then import and use:

use std::io;

use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

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

    println!("You guessed: {guess}");
}

Standard library (std) is available automatically but still requires explicit use statements.

Nested Paths

Combine multiple imports from the same crate/module:

use rand::Rng;
// --snip--
use std::{cmp::Ordering, io};
// --snip--

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

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

    let guess: u32 = guess.trim().parse().expect("Please type a number!");

    println!("You guessed: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

Use self to import both the parent and child:

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

The Glob Operator

Import all public items (use sparingly):

use std::collections::*;

Commonly used in test modules and when implementing prelude patterns. Can make code less clear and cause naming conflicts.

Separating Modules into Different Files

As modules grow, separate them into individual files for better organization. Unlike JavaScript/TypeScript where files are implicit modules, Rust requires explicit module declarations.

Moving Modules to Files

Starting from inline modules, extract them to separate files:

mod front_of_house;

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
}
pub mod hosting {
    pub fn add_to_waitlist() {}
}

The mod declaration loads the file once—it’s not an “include” operation. Other files reference the loaded module via its declared path.

Extracting Submodules

For submodules, create a directory structure:

pub mod hosting;
pub fn add_to_waitlist() {}

The file location must match the module hierarchy—hosting goes in src/front_of_house/hosting.rs because it’s a child of front_of_house.

File Path Conventions

Rust supports two file organization styles:

Modern style (recommended):

  • src/front_of_house.rs
  • src/front_of_house/hosting.rs

Legacy style (still supported):

  • src/front_of_house/mod.rs
  • src/front_of_house/hosting/mod.rs

Don’t mix styles within the same project to avoid confusion.

Summary

Rust’s module system provides explicit control over code organization and visibility:

  • Packages contain crates with Cargo.toml manifests
  • Crates are compilation units (binary or library)
  • Modules organize code with privacy controls (private by default)
  • Paths address items using :: syntax (absolute with crate:: or relative)
  • use creates local shortcuts to reduce path repetition
  • pub exposes items across module boundaries

This system enforces encapsulation at compile time, unlike JavaScript’s runtime-based privacy patterns.

Common Collections

Rust’s standard library provides heap-allocated collections that can grow or shrink at runtime. The three most commonly used are:

  • Vector (Vec<T>) - Dynamic arrays for storing values of the same type
  • String - UTF-8 encoded text collections
  • HashMap (HashMap<K, V>) - Key-value mappings using hash tables

Each collection has different performance characteristics and trade-offs. This chapter covers creation, updating, and reading operations for each type.

Storing Lists of Values with Vectors

Vec<T> is a growable array type that stores values contiguously in memory. All elements must be of the same type.

Creating Vectors

// Empty vector with type annotation
let v: Vec<i32> = Vec::new();

// Using the vec! macro with initial values
let v = vec![1, 2, 3];

Updating Vectors

let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);

The mut keyword is required for modification. Rust infers the type from the data.

Reading Elements

Two approaches for accessing elements:

let v = vec![1, 2, 3, 4, 5];

// Index syntax - panics on invalid index
let third: &i32 = &v[2];

// get method - returns Option<&T>
let third: Option<&i32> = v.get(2);

Use indexing when you want the program to crash on invalid access, use get() when you want to handle the error gracefully.

Borrowing Rules

let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6); // Error: cannot borrow as mutable
println!("The first element is: {first}");

Adding elements may require reallocation, invalidating existing references. The borrow checker prevents use-after-free errors.

Iteration

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

// Mutable iteration
let mut v = vec![100, 32, 57];
for i in &mut v {
    *i += 50;
}

Storing Multiple Types with Enums

Vectors can only store one type, but enums allow storing variants of different types:

enum SpreadsheetCell {
    Int(i32),
    Float(f64),
    Text(String),
}

let row = vec![
    SpreadsheetCell::Int(3),
    SpreadsheetCell::Text(String::from("blue")),
    SpreadsheetCell::Float(10.12),
];

For truly dynamic types unknown at compile time, use trait objects (covered in Chapter 18).

Memory Management

Vectors are automatically freed when they go out of scope, along with their contents. The borrow checker ensures references remain valid.

{
    let v = vec![1, 2, 3, 4];
    // v is valid here
} // v goes out of scope and is freed

Storing UTF-8 Encoded Text with Strings

Rust has two main string types: str (string slice, usually seen as &str) and String (owned, growable). Both are UTF-8 encoded.

  • &str - Immutable reference to string data, often stored in binary
  • String - Growable, heap-allocated, owned string type

Creating Strings

// Empty string
let mut s = String::new();

// From string literal
let s = "initial contents".to_string();
let s = String::from("initial contents");

// UTF-8 support
let hello = String::from("Здравствуйте");
let hello = String::from("नमस्ते");

Updating Strings

let mut s = String::from("foo");

// Append string slice
s.push_str("bar");

// Append single character  
s.push('l');

// Concatenation with + operator
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // s1 is moved, s2 can still be used

// Multiple concatenation with format! macro
let s = format!("{s1}-{s2}-{s3}");

The + operator uses the add method with signature fn add(self, s: &str) -> String, taking ownership of the left operand.

String Indexing is Not Allowed

let s1 = String::from("hello");
let h = s1[0]; // Error: doesn't implement Index<{integer}>

Reasons:

  1. Variable byte encoding - UTF-8 characters can be 1-4 bytes
  2. Performance - Would require O(n) time to find nth character
  3. Ambiguity - What should be returned? Byte, scalar value, or grapheme cluster?

Internal Representation

Strings are Vec<u8> wrappers with UTF-8 guarantees:

let hello = "Здравствуйте"; // 12 chars, 24 bytes
let hello = "नमस्ते";      // 4 graphemes, 6 scalar values, 18 bytes

String Slicing

Use ranges carefully - slicing must occur at valid UTF-8 boundaries:

let hello = "Здравствуйте";
let s = &hello[0..4]; // "Зд" (each char is 2 bytes)

// This would panic at runtime:
// let s = &hello[0..1]; // Invalid UTF-8 boundary

Iterating Over Strings

// By Unicode scalar values (char)
for c in "Зд".chars() {
    println!("{c}"); // З, д
}

// By bytes
for b in "Зд".bytes() {
    println!("{b}"); // 208, 151, 208, 180
}

// For grapheme clusters, use external crates

String Methods

Key methods for string manipulation:

let s = String::from("hello world");

// Search
s.contains("world");  // true

// Replace
s.replace("world", "rust"); // "hello rust"

// Split
s.split_whitespace(); // iterator over words

Unlike JavaScript strings, Rust strings are not indexed due to UTF-8 complexity. Use iteration methods or careful slicing instead.

Storing Keys with Associated Values in Hash Maps

HashMap<K, V> stores key-value pairs using a hashing function. Keys must implement Eq and Hash traits.

Creating Hash Maps

use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

Hash maps are not included in the prelude, require explicit use. No built-in macro like vec!.

Accessing Values

use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);

// Returns Option<&V>
let score = scores.get("Blue").copied().unwrap_or(0);

// Iterate over key-value pairs
for (key, value) in &scores {
    println!("{key}: {value}");
}

Ownership Rules

use std::collections::HashMap;

let field_name = String::from("Favorite color");
let field_value = String::from("Blue");

let mut map = HashMap::new();
map.insert(field_name, field_value);
// field_name and field_value are moved, no longer valid

// For Copy types like i32, values are copied
// For references, values must live as long as the hash map

Updating Hash Maps

Overwriting values:

scores.insert(String::from("Blue"), 25); // Replaces existing value

Insert only if key doesn’t exist:

scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50); // Won't insert, Blue exists

Update based on old value:

let text = "hello world wonderful world";
let mut map = HashMap::new();

for word in text.split_whitespace() {
    let count = map.entry(word).or_insert(0);
    *count += 1; // Dereference to modify the value
}

The entry method returns an Entry enum that represents a value that might or might not exist. or_insert returns a mutable reference to the value.

Performance Notes

  • Default hasher is SipHash (cryptographically secure, DoS resistant)
  • For performance-critical code, consider alternative hashers via BuildHasher trait
  • All keys must be same type, all values must be same type

Common Patterns

// Check if key exists
if scores.contains_key("Blue") { /* ... */ }

// Remove key-value pair
scores.remove("Blue");

// Get mutable reference to value
if let Some(score) = scores.get_mut("Blue") {
    *score += 10;
}

// Merge two hash maps
for (k, v) in other_map {
    scores.insert(k, v);
}

Hash maps provide O(1) average-case performance for insertions, deletions, and lookups, making them suitable for caching, counting, and indexing operations common in web applications.

Summary

Vectors, strings, and hash maps will provide a large amount of functionality necessary in programs when you need to store, access, and modify data. Here are some exercises you should now be equipped to solve:

  1. Given a list of integers, use a vector and return the median (when sorted, the value in the middle position) and mode (the value that occurs most often; a hash map will be helpful here) of the list.
  2. Convert strings to pig latin. The first consonant of each word is moved to the end of the word and ay is added, so first becomes irst-fay. Words that start with a vowel have hay added to the end instead (apple becomes apple-hay). Keep in mind the details about UTF-8 encoding!
  3. Using a hash map and vectors, create a text interface to allow a user to add employee names to a department in a company; for example, “Add Sally to Engineering” or “Add Amir to Sales.” Then let the user retrieve a list of all people in a department or all people in the company by department, sorted alphabetically.

The standard library API documentation describes methods that vectors, strings, and hash maps have that will be helpful for these exercises!

We’re getting into more complex programs in which operations can fail, so it’s a perfect time to discuss error handling. We’ll do that next!

Error Handling

Rust categorizes errors into two types:

  • Recoverable errors - Use Result<T, E> to handle expected failures (file not found, network timeout)
  • Unrecoverable errors - Use panic! macro for bugs and invariant violations (array bounds, null pointer dereference)

Unlike languages with exceptions, Rust makes error handling explicit through the type system. This forces you to decide how to handle each potential failure point, improving reliability and preventing silent failures.

This chapter covers when to use panic! vs Result<T, E> and how to effectively propagate and handle errors.

Unrecoverable Errors with panic!

The panic! macro immediately terminates the program when encountering unrecoverable errors. By default, it unwinds the stack, cleaning up data from each function before exiting.

Panic Behavior Configuration

[profile.release]
panic = 'abort'  # Skip unwinding, let OS clean up (smaller binary)

Explicit Panic

fn main() {
    panic!("crash and burn");
}

Output:

thread 'main' panicked at 'crash and burn', src/main.rs:2:5

Panic from Invalid Operations

fn main() {
    let v = vec![1, 2, 3];
    v[99]; // Index out of bounds triggers panic
}

Rust prevents buffer overreads by panicking on invalid array access, unlike C where this causes undefined behavior.

Debugging with Backtraces

Set RUST_BACKTRACE=1 to get a stack trace:

$ RUST_BACKTRACE=1 cargo run
thread 'main' panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
stack backtrace:
   0: rust_begin_unwind
   1: core::panicking::panic_fmt
   2: core::panicking::panic_bounds_check
   3: <usize as core::slice::index::SliceIndex<[T]>>::index
   4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
   5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
   6: panic::main  // <-- Your code here
             at ./src/main.rs:4:6
   7: core::ops::function::FnOnce::call_once

Start debugging from the first line mentioning your code (line 6 above). Lines above are library code that called your code; lines below are code that called your function.

When to Use Panic

Use panic! for:

  • Programming errors (bugs in your logic)
  • Violated invariants that should never happen
  • Security vulnerabilities from invalid data
  • Contract violations in functions

Example:

fn get_user(id: u32) -> User {
    if id == 0 {
        panic!("User ID cannot be zero"); // Contract violation
    }
    // ... rest of function
}

For recoverable errors (network failures, file not found), use Result<T, E> instead.

Recoverable Errors with Result

The Result<T, E> enum handles recoverable errors:

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

Basic Usage

use std::fs::File;

fn main() {
    let greeting_file_result = File::open("hello.txt");
    
    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => panic!("Problem opening file: {error:?}"),
    };
}

Handling Different Error Types

use std::fs::File;
use std::io::ErrorKind;

let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
    if error.kind() == ErrorKind::NotFound {
        File::create("hello.txt").unwrap_or_else(|error| {
            panic!("Problem creating file: {error:?}");
        })
    } else {
        panic!("Problem opening file: {error:?}");
    }
});

Shortcuts: unwrap and expect

// unwrap - panic with default message on error
let f = File::open("hello.txt").unwrap();

// expect - panic with custom message
let f = File::open("hello.txt")
    .expect("hello.txt should be included in this project");

Use expect over unwrap in production code for better debugging.

Error Propagation

Manual propagation:

fn read_username_from_file() -> Result<String, io::Error> {
    let username_file_result = File::open("hello.txt");
    
    let mut username_file = match username_file_result {
        Ok(file) => file,
        Err(e) => return Err(e),
    };
    
    let mut username = String::new();
    
    match username_file.read_to_string(&mut username) {
        Ok(_) => Ok(username),
        Err(e) => Err(e),
    }
}

The ? Operator

The ? operator simplifies error propagation:

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username_file = File::open("hello.txt")?;
    let mut username = String::new();
    username_file.read_to_string(&mut username)?;
    Ok(username)
}

// Even more concise with method chaining
fn read_username_from_file() -> Result<String, io::Error> {
    let mut username = String::new();
    File::open("hello.txt")?.read_to_string(&mut username)?;
    Ok(username)
}

// Using standard library convenience function
fn read_username_from_file() -> Result<String, io::Error> {
    fs::read_to_string("hello.txt")
}

The ? operator:

  1. If Ok(value) - returns value
  2. If Err(e) - returns early with Err(e) converted via From trait

? Operator Constraints

? can only be used in functions returning compatible types:

fn main() {
    let greeting_file = File::open("hello.txt")?; // Error: main returns ()
}

Fix by changing return type:

use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    let greeting_file = File::open("hello.txt")?;
    Ok(())
}

Using ? with Option

fn last_char_of_first_line(text: &str) -> Option<char> {
    text.lines().next()?.chars().last()
}

Common Patterns

Convert between Result and Option:

// Result to Option
let opt: Option<String> = File::open("hello.txt").ok();

// Option to Result
let result: Result<String, &str> = some_option.ok_or("value was None");

Chaining operations:

let processed = input
    .parse::<i32>()?
    .checked_mul(2)
    .ok_or("overflow")?;

Early returns in main:

fn main() -> Result<(), Box<dyn Error>> {
    let config = load_config()?;
    let data = fetch_data(&config)?;
    process_data(data)?;
    Ok(())
}

The ? operator makes error handling ergonomic while maintaining explicit control flow, similar to how async/await improves Promise handling in TypeScript.

To panic! or Not to panic!

Default choice: Return Result - gives calling code flexibility to decide how to handle errors.

Use panic! when:

  • Examples/prototypes - unwrap() and expect() are acceptable placeholders
  • Tests - failed assertions should terminate the test
  • Impossible states - when you have more information than the compiler
  • Contract violations - invalid inputs that should never happen
  • Security vulnerabilities - continuing could be dangerous

Examples and Prototyping

// Acceptable in examples and prototypes
let config: Config = std::env::args().collect().parse().unwrap();
let file = File::open("config.toml").expect("config file must exist");

Use expect() over unwrap() to provide context for debugging.

When You Know More Than the Compiler

use std::net::IpAddr;

let home: IpAddr = "127.0.0.1"
    .parse()
    .expect("Hardcoded IP address should be valid");

The string is hardcoded and valid, but the compiler can’t guarantee this.

Guidelines for Library Design

Panic for programming errors:

pub fn get_element(slice: &[i32], index: usize) -> i32 {
    if index >= slice.len() {
        panic!("Index {} out of bounds for slice of length {}", index, slice.len());
    }
    slice[index]
}

Return Result for expected failures:

pub fn parse_config(data: &str) -> Result<Config, ConfigError> {
    // Handle malformed data gracefully
}

pub fn fetch_url(url: &str) -> Result<Response, NetworkError> {
    // Network calls can fail in normal operation
}

Type-Based Validation

Create custom types to encode invariants:

pub struct Guess {
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 || value > 100 {
            panic!("Guess must be between 1 and 100, got {}", value);
        }
        
        Guess { value }
    }
    
    pub fn value(&self) -> i32 {
        self.value
    }
}

// Functions can now safely assume valid ranges
fn process_guess(guess: Guess) {
    // No need to validate - type system guarantees validity
}

Decision Matrix

ScenarioChoiceRationale
User input validationResultExpected to be invalid sometimes
Network requestsResultCan fail due to external factors
File operationsResultFiles may not exist or lack permissions
Array bounds checkingpanic!Programming error, should never happen
Parsing hardcoded stringsexpect()Invalid data indicates programming error
Library contract violationspanic!Caller passed invalid arguments

Modern Error Handling Patterns

Layered error handling:

// Low-level: specific errors
fn read_config_file() -> Result<String, io::Error> { /* ... */ }

// Mid-level: domain errors
fn parse_config(content: String) -> Result<Config, ConfigError> { /* ... */ }

// High-level: application errors
fn initialize() -> Result<App, Box<dyn Error>> {
    let content = read_config_file()?;
    let config = parse_config(content)?;
    Ok(App::new(config))
}

Error context chaining:

use anyhow::{Context, Result};

fn load_settings() -> Result<Settings> {
    let content = fs::read_to_string("settings.toml")
        .context("Failed to read settings file")?;
    
    toml::from_str(&content)
        .context("Failed to parse settings TOML")
}

The goal is to make error handling predictable and maintainable, similar to how TypeScript’s strict null checks prevent runtime errors by making nullability explicit.

Generic Types, Traits, and Lifetimes

Rust’s generics provide compile-time polymorphism through abstract type parameters. This chapter covers three key concepts: generic types for code reuse, traits for shared behavior contracts, and lifetimes for memory safety guarantees.

You’ve already used generics with Option<T>, Vec<T>, HashMap<K, V>, and Result<T, E>. Now you’ll learn to define your own generic types, functions, and methods.

Removing Duplication by Extracting a Function

Before exploring generics, let’s examine the pattern of extracting common functionality. Consider finding the largest number in a list:

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let mut largest = &number_list[0];

    for number in &number_list {
        if number > largest {
            largest = number;
        }
    }

    println!("The largest number is {largest}");
    assert_eq!(*largest, 100);
}

To handle multiple lists without code duplication, extract the logic into a function:

fn largest(list: &[i32]) -> &i32 {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest(&number_list);
    println!("The largest number is {result}");
    assert_eq!(*result, 100);

    let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];

    let result = largest(&number_list);
    println!("The largest number is {result}");
    assert_eq!(*result, 6000);
}

The refactoring process:

  1. Identify duplicate code
  2. Extract into function body with appropriate signature
  3. Replace duplicated code with function calls

This same pattern applies to generics—replacing specific types with abstract placeholders that work across multiple types.

Generic Data Types

Generics allow you to write code that works with multiple types while maintaining type safety. You can use generics in function signatures, structs, enums, and methods.

Function Definitions

Generic functions use type parameters in angle brackets. Consider these two functions that find the largest value:

fn largest_i32(list: &[i32]) -> &i32 {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn largest_char(list: &[char]) -> &char {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest_i32(&number_list);
    println!("The largest number is {result}");
    assert_eq!(*result, 100);

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest_char(&char_list);
    println!("The largest char is {result}");
    assert_eq!(*result, 'y');
}

Combine them using a generic type parameter T:

fn largest<T>(list: &[T]) -> &T {

This declares a function generic over type T, taking a slice of T and returning a reference to T.

fn largest<T>(list: &[T]) -> &T {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest(&number_list);
    println!("The largest number is {result}");

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest(&char_list);
    println!("The largest char is {result}");
}

This fails to compile because not all types support comparison:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `&T`
 --> src/main.rs:5:17
  |
5 |         if item > largest {
  |            ---- ^ ------- &T
  |            |
  |            &T
  |
help: consider restricting type parameter `T` with trait `PartialOrd`
  |
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
  |             ++++++++++++++++++++++

For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error

The error indicates you need trait bounds (covered in the next section) to constrain T to comparable types.

Struct Definitions

Structs can be generic over one or more types:

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let integer = Point { x: 5, y: 10 };
    let float = Point { x: 1.0, y: 4.0 };
}

Both fields must be the same type T. This won’t compile:

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let wont_work = Point { x: 5, y: 4.0 };
}

For different types, use multiple type parameters:

struct Point<T, U> {
    x: T,
    y: U,
}

fn main() {
    let both_integer = Point { x: 5, y: 10 };
    let both_float = Point { x: 1.0, y: 4.0 };
    let integer_and_float = Point { x: 5, y: 4.0 };
}

Enum Definitions

Enums can hold generic data types:

enum Option<T> {
    Some(T),
    None,
}

Multiple type parameters are common:

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

Method Definitions

Implement methods on generic structs by declaring the type parameter after impl:

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };

    println!("p.x = {}", p.x());
}

You can implement methods only for specific concrete types:

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };

    println!("p.x = {}", p.x());
}

Method type parameters can differ from struct type parameters:

struct Point<X1, Y1> {
    x: X1,
    y: Y1,
}

impl<X1, Y1> Point<X1, Y1> {
    fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

fn main() {
    let p1 = Point { x: 5, y: 10.4 };
    let p2 = Point { x: "Hello", y: 'c' };

    let p3 = p1.mixup(p2);

    println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}

Performance

Generics have zero runtime cost due to monomorphization—the compiler generates specific versions for each concrete type used:

let integer = Some(5);
let float = Some(5.0);

The compiler generates specialized versions:

enum Option_i32 {
    Some(i32),
    None,
}

enum Option_f64 {
    Some(f64),
    None,
}

fn main() {
    let integer = Option_i32::Some(5);
    let float = Option_f64::Some(5.0);
}

This eliminates runtime overhead while maintaining type safety.

Traits: Defining Shared Behavior

Traits define shared behavior contracts that types can implement. They’re similar to interfaces in other languages but with some unique features.

Defining a Trait

A trait groups method signatures that define required behavior:

pub trait Summary {
    fn summarize(&self) -> String;
}

The trait declaration uses the trait keyword followed by method signatures. Each implementing type must provide its own implementation.

Implementing a Trait on a Type

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

impl Summary for SocialPost {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

Use the trait like any other method:

use aggregator::{SocialPost, Summary};

fn main() {
    let post = SocialPost {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        repost: false,
    };

    println!("1 new post: {}", post.summarize());
}

Orphan rule: You can only implement a trait on a type if either the trait or the type (or both) are local to your crate. This prevents conflicting implementations.

Default Implementations

Traits can provide default method implementations:

pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {}

pub struct SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

impl Summary for SocialPost {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

Use an empty impl block to accept defaults:

impl Summary for NewsArticle {}

Default implementations can call other trait methods:

pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

pub struct SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

impl Summary for SocialPost {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
}

Traits as Parameters

Use the impl Trait syntax for function parameters:

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

impl Summary for SocialPost {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

Trait Bound Syntax

The full trait bound syntax is more verbose but more powerful:

pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

Use trait bounds when you need multiple parameters of the same type:

pub fn notify<T: Summary>(item1: &T, item2: &T) {

Multiple Trait Bounds

Specify multiple trait bounds with +:

pub fn notify(item: &(impl Summary + Display)) {

Or with generic syntax:

pub fn notify<T: Summary + Display>(item: &T) {

Where Clauses

For complex trait bounds, use where clauses for clarity:

fn some_function<T, U>(t: &T, u: &U) -> i32
where
    T: Display + Clone,
    U: Clone + Debug,
{
    unimplemented!()
}

Returning Types That Implement Traits

Return trait implementations without specifying concrete types:

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

impl Summary for SocialPost {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

fn returns_summarizable() -> impl Summary {
    SocialPost {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        repost: false,
    }
}

Limitation: You can only return a single concrete type, not different types conditionally.

Conditional Implementation

Implement methods conditionally based on trait bounds:

use std::fmt::Display;

struct Pair<T> {
    x: T,
    y: T,
}

impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
        Self { x, y }
    }
}

impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("The largest member is x = {}", self.x);
        } else {
            println!("The largest member is y = {}", self.y);
        }
    }
}

Blanket implementations implement a trait for any type that satisfies trait bounds:

impl<T: Display> ToString for T {
    // --snip--
}

This allows calling to_string() on any type implementing Display:

let s = 3.to_string();

Traits enable compile-time polymorphism while maintaining type safety and performance.

Validating References with Lifetimes

Lifetimes ensure references remain valid for their required duration. Every reference has a lifetime—the scope where it’s valid. Most lifetimes are inferred, but explicit annotation is required when relationships between reference lifetimes are ambiguous.

Preventing Dangling References

Lifetimes prevent dangling references by ensuring referenced data outlives the reference:

fn main() {
    let r;

    {
        let x = 5;
        r = &x;
    }

    println!("r: {r}");
}

This fails because x is destroyed before r is used:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `x` does not live long enough
 --> src/main.rs:6:13
  |
5 |         let x = 5;
  |             - binding `x` declared here
6 |         r = &x;
  |             ^^ borrowed value does not live long enough
7 |     }
  |     - `x` dropped here while still borrowed
8 |
9 |     println!("r: {r}");
  |                  --- borrow later used here

For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error

The Borrow Checker

The borrow checker compares scopes to validate borrows:

fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {r}");   //          |
}                         // ---------+

The reference r (lifetime 'a) outlives the referenced data x (lifetime 'b).

Fixed version:

fn main() {
    let x = 5;            // ----------+-- 'b
                          //           |
    let r = &x;           // --+-- 'a  |
                          //   |       |
    println!("r: {r}");   //   |       |
                          // --+       |
}                         // ----------+

Generic Lifetimes in Functions

Functions returning references need lifetime parameters when the compiler can’t determine the relationship between input and output lifetimes:

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}

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

Error:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0106]: missing lifetime specifier
 --> src/main.rs:9:33
  |
9 | fn longest(x: &str, y: &str) -> &str {
  |               ----     ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
  |
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error

Lifetime Annotation Syntax

Lifetime parameters start with an apostrophe and are typically lowercase:

&i32        // a reference
&'a i32     // a reference with explicit lifetime
&'a mut i32 // a mutable reference with explicit lifetime

Lifetime Annotations in Function Signatures

Declare lifetime parameters in angle brackets:

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}

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

This signature means:

  • Both parameters live at least as long as lifetime 'a
  • The returned reference will live at least as long as lifetime 'a
  • The actual lifetime is the smaller of the two input lifetimes

Example usage:

fn main() {
    let string1 = String::from("long string is long");

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

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

Invalid usage:

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}");
}

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

Thinking in Terms of Lifetimes

Only specify lifetime parameters for references that affect the output. If a function always returns the first parameter, the second parameter doesn’t need a lifetime annotation:

fn main() {
    let string1 = String::from("abcd");
    let string2 = "efghijklmnopqrstuvwxyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}

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

Returning references to values created within the function creates dangling references:

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}

fn longest<'a>(x: &str, y: &str) -> &'a str {
    let result = String::from("really long string");
    result.as_str()
}

Lifetime Annotations in Struct Definitions

Structs holding references need lifetime annotations:

struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}

This ensures the struct instance can’t outlive the reference it holds.

Lifetime Elision

Three rules eliminate the need for explicit lifetime annotations in common cases:

  1. Input lifetimes: Each reference parameter gets its own lifetime
  2. Single input: If there’s exactly one input lifetime, it’s assigned to all outputs
  3. Methods: If one parameter is &self or &mut self, its lifetime is assigned to all outputs

Examples:

// Rule 1: fn foo<'a>(x: &'a i32)
fn first_word(s: &str) -> &str {

// Rule 2: fn foo<'a>(x: &'a i32) -> &'a i32  
fn first_word<'a>(s: &'a str) -> &'a str {

For multiple inputs without self, explicit annotations are required:

fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {  // Error: can't determine output lifetime

Lifetime Annotations in Method Definitions

Struct lifetime parameters must be declared after impl:

struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {announcement}");
        self.part
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}

Third elision rule example:

struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {announcement}");
        self.part
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}

The Static Lifetime

The 'static lifetime indicates data living for the entire program duration:

let s: &'static str = "I have a static lifetime.";

String literals are stored in the binary and have 'static lifetime. Use 'static only when data truly lives for the program’s entire duration.

Generics, Trait Bounds, and Lifetimes Together

Example combining all three concepts:

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest_with_an_announcement(
        string1.as_str(),
        string2,
        "Today is someone's birthday!",
    );
    println!("The longest string is {result}");
}

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: Display,
{
    println!("Announcement! {ann}");
    if x.len() > y.len() { x } else { y }
}

This function:

  • Is generic over type T (with Display trait bound)
  • Has lifetime parameter 'a for references
  • Ensures the returned reference lives as long as both inputs

Lifetimes provide compile-time memory safety guarantees without runtime overhead.

Writing Automated Tests

Testing validates that code behaves as intended. Rust’s type system catches many bugs at compile time, but cannot verify business logic correctness. Automated tests ensure your code produces expected outputs for given inputs.

Rust provides built-in testing framework with attributes, macros, and execution control. This chapter covers test organization, writing effective tests, and test execution strategies.

Test Structure

Tests in Rust follow a standard pattern:

  • Setup: Arrange necessary data or state
  • Exercise: Execute the code under test
  • Assert: Verify results match expectations

The #[test] attribute marks functions as tests, and cargo test runs all tests in parallel by default.

How to Write Tests

Test functions use the #[test] attribute and typically follow the arrange-act-assert pattern. Rust provides several assertion macros and the #[should_panic] attribute for testing error conditions.

Test Function Basics

Generate a new library project to explore test structure:

$ cargo new adder --lib
pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}

Run tests with cargo test:

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.57s
     Running unittests src/lib.rs (target/debug/deps/adder-01ad14159ff659ab)

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Test functions panic on failure. Each test runs in its own thread—when the main thread detects a test thread has died, the test is marked as failed.

Example with passing and failing tests:

pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn exploration() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }

    #[test]
    fn another() {
        panic!("Make this test fail");
    }
}

Assertion Macros

assert! Macro

Verifies a condition evaluates to true:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn larger_can_hold_smaller() {
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(larger.can_hold(&smaller));
    }
}

assert_eq! and assert_ne! Macros

Test equality and inequality with better error messages:

pub fn add_two(a: u64) -> u64 {
    a + 2
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_adds_two() {
        let result = add_two(2);
        assert_eq!(result, 4);
    }
}

These macros use debug formatting, so tested values must implement PartialEq and Debug traits. Use #[derive(PartialEq, Debug)] for custom types.

Custom Failure Messages

Add custom messages with format strings:

pub fn greeting(name: &str) -> String {
    String::from("Hello!")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn greeting_contains_name() {
        let result = greeting("Carol");
        assert!(
            result.contains("Carol"),
            "Greeting did not contain name, value was `{result}`"
        );
    }
}

Testing Error Conditions

#[should_panic] Attribute

Test that code panics under certain conditions:

pub struct Guess {
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 || value > 100 {
            panic!("Guess value must be between 1 and 100, got {value}.");
        }

        Guess { value }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[should_panic]
    fn greater_than_100() {
        Guess::new(200);
    }
}

For more precise testing, specify expected panic message:

pub struct Guess {
    value: i32,
}

// --snip--

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 {
            panic!(
                "Guess value must be greater than or equal to 1, got {value}."
            );
        } else if value > 100 {
            panic!(
                "Guess value must be less than or equal to 100, got {value}."
            );
        }

        Guess { value }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[should_panic(expected = "less than or equal to 100")]
    fn greater_than_100() {
        Guess::new(200);
    }
}

Using Result<T, E> in Tests

Alternative to panicking—return Result types:

pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() -> Result<(), String> {
        let result = add(2, 2);

        if result == 4 {
            Ok(())
        } else {
            Err(String::from("two plus two does not equal four"))
        }
    }
}

Return Ok(()) for success, Err for failure. Enables use of the ? operator for error propagation. Cannot be combined with #[should_panic]—use assert!(value.is_err()) instead.

Controlling How Tests Are Run

cargo test compiles and runs tests with customizable behavior through command-line options. Options for cargo test come before --, while options for the test binary come after --.

Parallel vs Sequential Execution

Tests run in parallel by default using threads. To run sequentially or control thread count:

$ cargo test -- --test-threads=1

Showing Function Output

By default, passing tests capture output (e.g., println!). Failed tests show captured output. To see output from passing tests:

$ cargo test -- --show-output

Example test with output:

fn prints_and_returns_10(a: i32) -> i32 {
    println!("I got the value {a}");
    10
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn this_test_will_pass() {
        let value = prints_and_returns_10(4);
        assert_eq!(value, 10);
    }

    #[test]
    fn this_test_will_fail() {
        let value = prints_and_returns_10(8);
        assert_eq!(value, 5);
    }
}

Running Subset of Tests

Single Tests

Run specific test by name:

$ cargo test one_hundred

Filtering Tests

Run multiple tests matching a pattern:

$ cargo test add  // Runs tests with "add" in the name

Module names are part of test names, so you can filter by module.

Ignoring Tests

Mark expensive tests to skip during normal runs:

pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }

    #[test]
    #[ignore]
    fn expensive_test() {
        // code that takes an hour to run
    }
}

Run only ignored tests:

$ cargo test -- --ignored

Run all tests (including ignored):

$ cargo test -- --include-ignored

Test Organization

Rust categorizes tests as unit tests (small, focused, test one module in isolation, can test private interfaces) and integration tests (external to your library, use public API only, test multiple modules together).

Unit Tests

Place unit tests in each file with the code they test, in a tests module annotated with #[cfg(test)].

The Tests Module and #[cfg(test)]

The #[cfg(test)] annotation compiles and runs test code only with cargo test, not cargo build:

pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}

Testing Private Functions

Rust allows testing private functions since tests are just Rust code in the same crate:

pub fn add_two(a: u64) -> u64 {
    internal_adder(a, 2)
}

fn internal_adder(left: u64, right: u64) -> u64 {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn internal() {
        let result = internal_adder(2, 2);
        assert_eq!(result, 4);
    }
}

The use super::*; brings parent module items into scope.

Integration Tests

Create integration tests in a tests directory at the project root (alongside src). Each file in tests is compiled as a separate crate.

The tests Directory

Project structure:

adder
├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
    └── integration_test.rs
use adder::add_two;

#[test]
fn it_adds_two() {
    let result = add_two(2);
    assert_eq!(result, 4);
}

Each test file gets its own section in test output. Run specific integration test file:

$ cargo test --test integration_test

Submodules in Integration Tests

Files in tests directory are separate crates and don’t share the same behavior as src files.

For shared helper functions, use subdirectories instead of top-level files:

├── tests
    ├── common
    │   └── mod.rs      // Shared code
    └── integration_test.rs

Use from integration tests:

use adder::add_two;

mod common;

#[test]
fn it_adds_two() {
    common::setup();

    let result = add_two(2);
    assert_eq!(result, 4);
}

Integration Tests for Binary Crates

Binary crates with only src/main.rs cannot have integration tests that import functions with use statements. Structure binary projects with logic in src/lib.rs and a thin src/main.rs that calls the library functions.

Summary

Rust’s testing features support both detailed unit testing and broader integration testing. Unit tests verify individual modules (including private functions), while integration tests validate public API behavior. This testing strategy helps ensure code correctness as you refactor and extend functionality.

Building a Command Line Tool: grep Implementation

This chapter demonstrates Rust concepts through building a grep clone. The project combines:

  • Code organization and module system
  • Collections (vectors, strings)
  • Error handling patterns
  • Traits and lifetimes
  • Testing strategies
  • Closures and iterators (introduced here, detailed in Chapter 13)

Project Overview

We’ll build minigrep - a simplified version of the Unix grep utility that searches files for text patterns. The tool will:

  • Accept command line arguments (query string and file path)
  • Read file contents
  • Filter matching lines
  • Handle errors gracefully
  • Support case-insensitive search via environment variables
  • Write errors to stderr, results to stdout

This implementation showcases Rust’s performance, safety, and cross-platform capabilities for CLI tools, while demonstrating idiomatic patterns for real-world applications.

Command Line Arguments

Create the project and handle command line arguments:

$ cargo new minigrep
$ cd minigrep

Target usage: cargo run -- searchstring example-filename.txt

Reading Arguments

Use std::env::args to access command line arguments:

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    dbg!(args);
}

Key points:

  • env::args() returns an iterator over arguments
  • collect() converts the iterator to Vec<String>
  • First argument (args[0]) is always the binary name
  • Invalid Unicode in arguments will panic; use env::args_os() for OsString if needed

Storing Arguments

Extract the required arguments into variables:

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();

    let query = &args[1];
    let file_path = &args[2];

    println!("Searching for {query}");
    println!("In file {file_path}");
}

This basic implementation lacks error handling for insufficient arguments - we’ll address that in the refactoring section.

File Reading

Add file reading capability to complete the basic functionality:

I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.

How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!
use std::env;
use std::fs;

fn main() {
    // --snip--
    let args: Vec<String> = env::args().collect();

    let query = &args[1];
    let file_path = &args[2];

    println!("Searching for {query}");
    println!("In file {file_path}");

    let contents = fs::read_to_string(file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

Key points:

  • fs::read_to_string() returns Result<String, std::io::Error>
  • Currently using expect() for error handling (will improve in next section)
  • The main function is handling multiple responsibilities (argument parsing, file reading)

Current issues to address:

  1. Poor separation of concerns
  2. Inadequate error handling
  3. No validation of input arguments

The next section addresses these through refactoring.

Refactoring: Error Handling and Modularity

Current issues:

  1. main function has multiple responsibilities
  2. Configuration variables mixed with business logic
  3. Generic error messages using expect
  4. No input validation

Binary Project Organization Pattern

Standard Rust pattern for CLI applications:

  • main.rs: Command line parsing, configuration, calling run(), error handling
  • lib.rs: Application logic, testable functions

Extracting Configuration Parser

Extract argument parsing into a dedicated function:

use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let (query, file_path) = parse_config(&args);

    // --snip--

    println!("Searching for {query}");
    println!("In file {file_path}");

    let contents = fs::read_to_string(file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

fn parse_config(args: &[String]) -> (&str, &str) {
    let query = &args[1];
    let file_path = &args[2];

    (query, file_path)
}

Grouping Configuration Data

Use a struct instead of tuple for better semantics:

use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = parse_config(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    // --snip--

    println!("With text:\n{contents}");
}

struct Config {
    query: String,
    file_path: String,
}

fn parse_config(args: &[String]) -> Config {
    let query = args[1].clone();
    let file_path = args[2].clone();

    Config { query, file_path }
}

Note: Using clone() for simplicity. Chapter 13 covers more efficient approaches using iterators.

Constructor Pattern

Convert to associated function following Rust conventions:

use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");

    // --snip--
}

// --snip--

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn new(args: &[String]) -> Config {
        let query = args[1].clone();
        let file_path = args[2].clone();

        Config { query, file_path }
    }
}

Error Handling

Replace panic! with proper error handling:

use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    // --snip--
    fn new(args: &[String]) -> Config {
        if args.len() < 3 {
            panic!("not enough arguments");
        }
        // --snip--

        let query = args[1].clone();
        let file_path = args[2].clone();

        Config { query, file_path }
    }
}

Returning Results

Better practice: return Result instead of panicking:

use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}
use std::env;
use std::fs;
use std::process;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    // --snip--

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

Key pattern: unwrap_or_else with closure for custom error handling and controlled exit codes.

Extracting Business Logic

Separate program logic from main:

use std::env;
use std::fs;
use std::process;

fn main() {
    // --snip--

    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    run(config);
}

fn run(config: Config) {
    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

// --snip--

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

Propagating Errors

Make run return Result for proper error propagation:

use std::env;
use std::fs;
use std::process;
use std::error::Error;

// --snip--


fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    run(config);
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    println!("With text:\n{contents}");

    Ok(())
}

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

Using Box<dyn Error> trait object for flexible error types. The ? operator propagates errors instead of panicking.

Handle the returned Result in main:

use std::env;
use std::error::Error;
use std::fs;
use std::process;

fn main() {
    // --snip--

    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    if let Err(e) = run(config) {
        println!("Application error: {e}");
        process::exit(1);
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    println!("With text:\n{contents}");

    Ok(())
}

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

Creating Library Crate

Move business logic to lib.rs for better testability:

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    unimplemented!();
}
use std::env;
use std::error::Error;
use std::fs;
use std::process;

// --snip--
use minigrep::search;

fn main() {
    // --snip--
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        println!("Application error: {e}");
        process::exit(1);
    }
}

// --snip--


struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    for line in search(&config.query, &contents) {
        println!("{line}");
    }

    Ok(())
}

This structure enables:

  • Unit testing of business logic
  • Reusable library components
  • Clear separation of concerns
  • Better error handling patterns

Test-Driven Development

With logic separated into lib.rs, we can implement the search functionality using TDD.

Writing the Test

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    unimplemented!();
}

// --snip--

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}

Implementing to Pass

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    vec![]
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}

Lifetime Annotations

The function signature requires explicit lifetime annotation because the returned string slices reference the contents parameter, not query. This tells Rust that the returned data lives as long as the contents input.

Implementation Steps

Iterating Through Lines

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    for line in contents.lines() {
        // do something with line
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}

Filtering Lines

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    for line in contents.lines() {
        if line.contains(query) {
            // do something with line
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}

Collecting Results

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}

The implementation works with basic text search. Chapter 13 will show how to improve this using iterators for better performance and cleaner code.

Environment Variables

Add case-insensitive search controlled by environment variables.

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn case_sensitive() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }

    #[test]
    fn case_insensitive() {
        let query = "rUsT";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";

        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}

Implementation

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

pub fn search_case_insensitive<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    let query = query.to_lowercase();
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.to_lowercase().contains(&query) {
            results.push(line);
        }
    }

    results
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn case_sensitive() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }

    #[test]
    fn case_insensitive() {
        let query = "rUsT";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";

        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}

Key points:

  • to_lowercase() creates new String data, changing the type from &str to String
  • Need & when passing to contains() since it expects &str
  • This handles basic Unicode but isn’t 100% accurate for all cases

Configuration Integration

Add environment variable support to the Config struct:

use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

// --snip--


fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        println!("Application error: {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{line}");
    }

    Ok(())
}

Update the run function to use the appropriate search function:

use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

// --snip--


fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        println!("Application error: {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{line}");
    }

    Ok(())
}

Environment Variable Detection

use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        println!("Application error: {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{line}");
    }

    Ok(())
}

Usage patterns:

  • env::var() returns Result<String, VarError>
  • is_ok() returns true if variable is set (regardless of value)
  • We only care about presence, not the actual value

Testing

$ cargo run -- to poem.txt
Are you nobody, too?
How dreary to be somebody!
$ IGNORE_CASE=1 cargo run -- to poem.txt
Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!

PowerShell:

PS> $Env:IGNORE_CASE=1; cargo run -- to poem.txt
PS> Remove-Item Env:IGNORE_CASE  # To unset

This pattern allows flexible configuration without requiring command-line flags for every option.

Error Output to stderr

Proper CLI tools distinguish between regular output (stdout) and error messages (stderr) for better shell integration.

Current Problem

Our error messages currently go to stdout:

$ cargo run > output.txt
Problem parsing arguments: not enough arguments

The error appears in output.txt instead of the terminal, making debugging difficult when output is redirected.

Solution: eprintln! Macro

use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        eprintln!("Application error: {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{line}");
    }

    Ok(())
}

Testing the Fix

Error case with redirection:

$ cargo run > output.txt
Problem parsing arguments: not enough arguments

Now the error appears on terminal while output.txt remains empty.

Success case with redirection:

$ cargo run -- to poem.txt > output.txt

Terminal shows nothing, output.txt contains results:

Are you nobody, too?
How dreary to be somebody!

Summary

This implementation demonstrates:

  • Code organization: Clean separation between main.rs and lib.rs
  • Error handling: Proper Result types and error propagation
  • Testing: TDD approach with isolated, testable functions
  • CLI patterns: Command line args, environment variables, proper output streams
  • Rust features: Ownership, lifetimes, traits, and error handling

The resulting CLI tool follows Unix conventions and demonstrates production-ready patterns for Rust applications.

Functional Language Features: Iterators and Closures

Rust incorporates functional programming concepts including closures and iterators, both offering zero-cost abstractions with compile-time optimizations.

This chapter covers:

  • Closures: Anonymous functions that capture their environment
  • Iterators: Lazy evaluation for processing data sequences
  • Performance: Both compile to equivalent machine code as hand-written loops
  • Practical application: Refactoring I/O operations using functional patterns

These features enable expressive, high-performance code while maintaining Rust’s safety guarantees.

Closures: Anonymous Functions That Capture Their Environment

Closures are anonymous functions that capture values from their enclosing scope. Unlike regular functions, closures can access variables from the environment where they’re defined.

Environment Capture

Consider this scenario: a shirt giveaway system that assigns colors based on user preferences or inventory levels.

#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
    Red,
    Blue,
}

struct Inventory {
    shirts: Vec<ShirtColor>,
}

impl Inventory {
    fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
        user_preference.unwrap_or_else(|| self.most_stocked())
    }

    fn most_stocked(&self) -> ShirtColor {
        let mut num_red = 0;
        let mut num_blue = 0;

        for color in &self.shirts {
            match color {
                ShirtColor::Red => num_red += 1,
                ShirtColor::Blue => num_blue += 1,
            }
        }
        if num_red > num_blue {
            ShirtColor::Red
        } else {
            ShirtColor::Blue
        }
    }
}

fn main() {
    let store = Inventory {
        shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],
    };

    let user_pref1 = Some(ShirtColor::Red);
    let giveaway1 = store.giveaway(user_pref1);
    println!(
        "The user with preference {:?} gets {:?}",
        user_pref1, giveaway1
    );

    let user_pref2 = None;
    let giveaway2 = store.giveaway(user_pref2);
    println!(
        "The user with preference {:?} gets {:?}",
        user_pref2, giveaway2
    );
}

The closure || self.most_stocked() captures an immutable reference to self. The unwrap_or_else method calls this closure only when the Option is None, demonstrating lazy evaluation.

$ cargo run
   Compiling shirt-company v0.1.0 (file:///projects/shirt-company)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
     Running `target/debug/shirt-company`
The user with preference Some(Red) gets Red
The user with preference None gets Blue

Type Inference and Annotations

Closures don’t require explicit type annotations in most cases—the compiler infers types from usage:

use std::thread;
use std::time::Duration;

fn generate_workout(intensity: u32, random_number: u32) {
    let expensive_closure = |num: u32| -> u32 {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        num
    };

    if intensity < 25 {
        println!("Today, do {} pushups!", expensive_closure(intensity));
        println!("Next, do {} situps!", expensive_closure(intensity));
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                expensive_closure(intensity)
            );
        }
    }
}

fn main() {
    let simulated_user_specified_value = 10;
    let simulated_random_number = 7;

    generate_workout(simulated_user_specified_value, simulated_random_number);
}

Syntax comparison:

fn  add_one_v1   (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x|             { x + 1 };
let add_one_v4 = |x|               x + 1  ;

Once a closure’s types are inferred from first usage, they’re locked in:

fn main() {
    let example_closure = |x| x;

    let s = example_closure(String::from("hello"));
    let n = example_closure(5);
}
$ cargo run
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
error[E0308]: mismatched types
 --> src/main.rs:5:29
  |
5 |     let n = example_closure(5);
  |             --------------- ^- help: try using a conversion method: `.to_string()`
  |             |               |
  |             |               expected `String`, found integer
  |             arguments to this function are incorrect
  |
note: expected because the closure was earlier called with an argument of type `String`
 --> src/main.rs:4:29
  |
4 |     let s = example_closure(String::from("hello"));
  |             --------------- ^^^^^^^^^^^^^^^^^^^^^ expected because this argument is of type `String`
  |             |
  |             in this closure call
note: closure parameter defined here
 --> src/main.rs:2:28
  |
2 |     let example_closure = |x| x;
  |                            ^

For more information about this error, try `rustc --explain E0308`.
error: could not compile `closure-example` (bin "closure-example") due to 1 previous error

Capture Modes

Closures automatically choose the minimal capture mode needed:

Immutable borrow:

fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {list:?}");

    let only_borrows = || println!("From closure: {list:?}");

    println!("Before calling closure: {list:?}");
    only_borrows();
    println!("After calling closure: {list:?}");
}

Mutable borrow:

fn main() {
    let mut list = vec![1, 2, 3];
    println!("Before defining closure: {list:?}");

    let mut borrows_mutably = || list.push(7);

    borrows_mutably();
    println!("After calling closure: {list:?}");
}

Move semantics: Use move to force ownership transfer, commonly needed for thread boundaries:

use std::thread;

fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {list:?}");

    thread::spawn(move || println!("From thread: {list:?}"))
        .join()
        .unwrap();
}

Fn Traits

Closures implement one or more of these traits based on how they handle captured values:

  • FnOnce: Callable once, consumes captured values. All closures implement this.
  • FnMut: Callable multiple times, may mutate captured values.
  • Fn: Callable multiple times without mutation, safe for concurrent access.

Example from Option<T>:

impl<T> Option<T> {
    pub fn unwrap_or_else<F>(self, f: F) -> T
    where
        F: FnOnce() -> T
    {
        match self {
            Some(x) => x,
            None => f(),
        }
    }
}

sort_by_key requires FnMut since it calls the closure multiple times:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    list.sort_by_key(|r| r.width);
    println!("{list:#?}");
}

This won’t compile because the closure moves value out, implementing only FnOnce:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    let mut sort_operations = vec![];
    let value = String::from("closure called");

    list.sort_by_key(|r| {
        sort_operations.push(value);
        r.width
    });
    println!("{list:#?}");
}
$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
error[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure
  --> src/main.rs:18:30
   |
15 |     let value = String::from("closure called");
   |         ----- captured outer variable
16 |
17 |     list.sort_by_key(|r| {
   |                      --- captured by this `FnMut` closure
18 |         sort_operations.push(value);
   |                              ^^^^^ move occurs because `value` has type `String`, which does not implement the `Copy` trait
   |
help: consider cloning the value if the performance cost is acceptable
   |
18 |         sort_operations.push(value.clone());
   |                                   ++++++++

For more information about this error, try `rustc --explain E0507`.
error: could not compile `rectangles` (bin "rectangles") due to 1 previous error

Correct approach using mutable reference:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    let mut num_sort_operations = 0;
    list.sort_by_key(|r| {
        num_sort_operations += 1;
        r.width
    });
    println!("{list:#?}, sorted in {num_sort_operations} operations");
}

Processing a Series of Items with Iterators

Iterators implement lazy evaluation—they do nothing until consumed. This enables efficient chaining of transformations without intermediate allocations.

fn main() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();
}
fn main() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();

    for val in v1_iter {
        println!("Got: {val}");
    }
}

The Iterator Trait and next Method

All iterators implement the Iterator trait:

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;

    // methods with default implementations elided
}

The associated type Item defines what the iterator yields. Only next requires implementation—all other methods have default implementations.

#[cfg(test)]
mod tests {
    #[test]
    fn iterator_demonstration() {
        let v1 = vec![1, 2, 3];

        let mut v1_iter = v1.iter();

        assert_eq!(v1_iter.next(), Some(&1));
        assert_eq!(v1_iter.next(), Some(&2));
        assert_eq!(v1_iter.next(), Some(&3));
        assert_eq!(v1_iter.next(), None);
    }
}

Note: v1_iter must be mutable because next consumes items. The for loop handles this automatically by taking ownership.

Iterator creation methods:

  • iter(): Immutable references (&T)
  • into_iter(): Owned values (T)
  • iter_mut(): Mutable references (&mut T)

Consuming Adapters

Methods that call next and consume the iterator:

#[cfg(test)]
mod tests {
    #[test]
    fn iterator_sum() {
        let v1 = vec![1, 2, 3];

        let v1_iter = v1.iter();

        let total: i32 = v1_iter.sum();

        assert_eq!(total, 6);
    }
}

Iterator Adapters

Methods that transform iterators into other iterators. Must be consumed to execute:

fn main() {
    let v1: Vec<i32> = vec![1, 2, 3];

    v1.iter().map(|x| x + 1);
}
$ cargo run
   Compiling iterators v0.1.0 (file:///projects/iterators)
warning: unused `Map` that must be used
 --> src/main.rs:4:5
  |
4 |     v1.iter().map(|x| x + 1);
  |     ^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: iterators are lazy and do nothing unless consumed
  = note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
  |
4 |     let _ = v1.iter().map(|x| x + 1);
  |     +++++++

warning: `iterators` (bin "iterators") generated 1 warning
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.47s
     Running `target/debug/iterators`
fn main() {
    let v1: Vec<i32> = vec![1, 2, 3];

    let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();

    assert_eq!(v2, vec![2, 3, 4]);
}

Closures with Environment Capture

Iterator adapters commonly use closures that capture environment variables:

#[derive(PartialEq, Debug)]
struct Shoe {
    size: u32,
    style: String,
}

fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
    shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn filters_by_size() {
        let shoes = vec![
            Shoe {
                size: 10,
                style: String::from("sneaker"),
            },
            Shoe {
                size: 13,
                style: String::from("sandal"),
            },
            Shoe {
                size: 10,
                style: String::from("boot"),
            },
        ];

        let in_my_size = shoes_in_size(shoes, 10);

        assert_eq!(
            in_my_size,
            vec![
                Shoe {
                    size: 10,
                    style: String::from("sneaker")
                },
                Shoe {
                    size: 10,
                    style: String::from("boot")
                },
            ]
        );
    }
}

The filter closure captures shoe_size from the environment, demonstrating how iterators can access external state while maintaining functional programming patterns.

Improving Our I/O Project

This section demonstrates refactoring imperative code to use iterators, eliminating clone calls and improving readability through functional patterns.

Removing clone with Iterator Ownership

Original implementation with inefficient cloning:

use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        println!("Application error: {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{line}");
    }

    Ok(())
}

Refactoring to Accept an Iterator

Change main.rs to pass the iterator directly:

use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

fn main() {
    let config = Config::build(env::args()).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    // --snip--

    if let Err(e) = run(config) {
        eprintln!("Application error: {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{line}");
    }

    Ok(())
}

Update the function signature to accept any iterator returning String:

use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

fn main() {
    let config = Config::build(env::args()).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {err}");
        process::exit(1);
    });


    if let Err(e) = run(config) {
        eprintln!("Application error: {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    fn build(
        mut args: impl Iterator<Item = String>,
    ) -> Result<Config, &'static str> {
        // --snip--
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{line}");
    }

    Ok(())
}

Using Iterator Methods Instead of Indexing

Replace indexing with next() calls:

use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

fn main() {
    let config = Config::build(env::args()).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        eprintln!("Application error: {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    fn build(
        mut args: impl Iterator<Item = String>,
    ) -> Result<Config, &'static str> {
        args.next();

        let query = match args.next() {
            Some(arg) => arg,
            None => return Err("Didn't get a query string"),
        };

        let file_path = match args.next() {
            Some(arg) => arg,
            None => return Err("Didn't get a file path"),
        };

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{line}");
    }

    Ok(())
}

This eliminates cloning by taking ownership of iterator values, while improving error handling with explicit None checks.

Functional Style with Iterator Adapters

Original imperative search function:

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}

Functional refactor using iterator adapters:

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    contents
        .lines()
        .filter(|line| line.contains(query))
        .collect()
}

pub fn search_case_insensitive<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    let query = query.to_lowercase();
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.to_lowercase().contains(&query) {
            results.push(line);
        }
    }

    results
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn case_sensitive() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }

    #[test]
    fn case_insensitive() {
        let query = "rUsT";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";

        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}

Benefits:

  • Eliminates mutable state (results vector)
  • Enables potential parallelization
  • More concise and declarative
  • Better composability

Performance note: Both implementations compile to equivalent machine code due to Rust’s zero-cost abstractions.

Iterator vs. Loop Performance

Iterator adapters are zero-cost abstractions—they compile to the same assembly as hand-written loops. The functional style often provides better readability and composability without performance penalties.

For high-performance applications, prefer iterators for their:

  • Compile-time optimizations (loop unrolling, bounds check elimination)
  • Reduced cognitive load through declarative style
  • Built-in parallelization support with external crates

Comparing Performance: Loops vs. Iterators

Benchmark results comparing explicit for loops vs. iterator implementation searching for “the” in The Adventures of Sherlock Holmes:

test bench_search_for  ... bench:  19,620,300 ns/iter (+/- 915,700)
test bench_search_iter ... bench:  19,234,900 ns/iter (+/- 657,200)

Key findings:

  • Iterator version performs slightly better
  • Performance difference is negligible
  • Both compile to nearly identical assembly

Zero-Cost Abstractions

Iterators exemplify Rust’s zero-cost abstractions—high-level constructs that impose no runtime overhead. The compiler applies optimizations including:

  • Loop unrolling
  • Bounds check elimination
  • Inline expansion
  • Dead code elimination

These optimizations often produce assembly equivalent to hand-optimized code.

Summary

Closures and iterators provide functional programming patterns with systems-level performance. They enable expressive, maintainable code without sacrificing runtime efficiency—a core principle of Rust’s design philosophy.

Use iterators and closures confidently in performance-critical code. The abstractions compile away, leaving only the essential operations.

Now that we’ve improved the expressiveness of our I/O project, let’s look at some more features of cargo that will help us share the project with the world.

More About Cargo and Crates.io

Cargo provides advanced project management features beyond basic building and testing:

  • Release profiles: Customize build configurations for development and production
  • Publishing: Share libraries on crates.io
  • Workspaces: Manage multi-crate projects with shared dependencies
  • Binary installation: Install command-line tools from the ecosystem
  • Custom commands: Extend Cargo with additional functionality

For comprehensive documentation, see cargo docs.

Customizing Builds with Release Profiles

Release profiles control compiler optimization levels and debugging information. Cargo uses two main profiles:

  • dev: Used by cargo build (fast compilation, debugging info)
  • release: Used by cargo build --release (optimized, production-ready)
$ cargo build
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
$ cargo build --release
    Finished `release` profile [optimized] target(s) in 0.32s

Default Settings

[profile.dev]
opt-level = 0

[profile.release]
opt-level = 3

opt-level controls optimization intensity (0-3):

  • 0: No optimization, fastest compilation
  • 3: Maximum optimization, slower compilation

Custom Configuration

Override defaults in Cargo.toml:

[profile.dev]
opt-level = 1

This applies moderate optimization to development builds, trading compile time for runtime performance.

For complete configuration options, see Cargo’s profile documentation.

Publishing a Crate to Crates.io

We’ve used packages from crates.io as dependencies of our project, but you can also share your code with other people by publishing your own packages. The crate registry at crates.io distributes the source code of your packages, so it primarily hosts code that is open source.

Rust and Cargo have features that make your published package easier for people to find and use. We’ll talk about some of these features next and then explain how to publish a package.

Documentation Comments

Use /// for API documentation that generates HTML:

/// Adds one to the number given.
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
    x + 1
}

Generate docs: cargo doc --open

Standard Sections

  • Panics: Conditions that cause panics
  • Errors: Error types and causes for Result returns
  • Safety: Safety requirements for unsafe functions

Documentation Tests

Code in documentation comments runs as tests with cargo test:

   Doc-tests my_crate

running 1 test
test src/lib.rs - add_one (line 5) ... ok

Module Documentation

Use //! for crate/module-level documentation:

//! # My Crate
//!
//! `my_crate` is a collection of utilities to make performing certain
//! calculations more convenient.

/// Adds one to the number given.
// --snip--
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
    x + 1
}

Public API Design with pub use

Re-export items to create a clean public API regardless of internal structure:

//! # Art
//!
//! A library for modeling artistic concepts.

pub mod kinds {
    /// The primary colors according to the RYB color model.
    pub enum PrimaryColor {
        Red,
        Yellow,
        Blue,
    }

    /// The secondary colors according to the RYB color model.
    pub enum SecondaryColor {
        Orange,
        Green,
        Purple,
    }
}

pub mod utils {
    use crate::kinds::*;

    /// Combines two primary colors in equal amounts to create
    /// a secondary color.
    pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
        // --snip--
        unimplemented!();
    }
}

Without re-exports, users must write:

use art::kinds::PrimaryColor;
use art::utils::mix;
//! # Art
//!
//! A library for modeling artistic concepts.

pub use self::kinds::PrimaryColor;
pub use self::kinds::SecondaryColor;
pub use self::utils::mix;

pub mod kinds {
    // --snip--
    /// The primary colors according to the RYB color model.
    pub enum PrimaryColor {
        Red,
        Yellow,
        Blue,
    }

    /// The secondary colors according to the RYB color model.
    pub enum SecondaryColor {
        Orange,
        Green,
        Purple,
    }
}

pub mod utils {
    // --snip--
    use crate::kinds::*;

    /// Combines two primary colors in equal amounts to create
    /// a secondary color.
    pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
        SecondaryColor::Orange
    }
}

Now users can write:

use art::{PrimaryColor, mix};

Publishing Workflow

  1. Create account: Log in at crates.io with GitHub
  2. Get API token: From account settings, store with cargo login
  3. Add metadata to Cargo.toml:
[package]
name = "your_crate_name"
version = "0.1.0"
edition = "2024"
description = "A brief description of your crate's functionality."
license = "MIT OR Apache-2.0"
  1. Publish: cargo publish

Version Management

  • New versions: Update version in Cargo.toml, run cargo publish
  • Yanking: cargo yank --vers 1.0.1 (prevents new usage, doesn’t break existing)
  • Un-yanking: cargo yank --vers 1.0.1 --undo

Note: Published versions are permanent and cannot be deleted.

License Options

Use SPDX identifiers. Common choices:

  • MIT: Permissive
  • Apache-2.0: Permissive with patent grant
  • MIT OR Apache-2.0: Rust ecosystem standard

For custom licenses, use license-file instead of license.

Cargo Workspaces

Workspaces manage multiple related crates with shared dependencies and a unified build system.

Creating a Workspace

Create workspace structure:

$ mkdir add
$ cd add

Workspace Cargo.toml:

[workspace]
resolver = "3"

Add binary crate:

$ cargo new adder
     Created binary (application) `adder` package
      Adding `adder` as member of workspace at `file:///projects/add`

Workspace structure:

├── Cargo.lock
├── Cargo.toml
├── adder
│   ├── Cargo.toml
│   └── src
│       └── main.rs
└── target

Key points:

  • Single target directory for all workspace members
  • Shared Cargo.lock ensures consistent dependency versions
  • Build from workspace root with cargo build

Adding Library Crates

$ cargo new add_one --lib
     Created library `add_one` package

Library implementation:

// add_one/src/lib.rs
pub fn add_one(x: i32) -> i32 {
    x + 1
}

Workspace dependencies:

# adder/Cargo.toml
[dependencies]
add_one = { path = "../add_one" }

Using workspace dependencies:

fn main() {
    let num = 10;
    println!("Hello, world! {num} plus one is {}!", add_one::add_one(num));
}

Workspace Commands

Build workspace:

$ cargo build
   Compiling add_one v0.1.0 (file:///projects/add/add_one)
   Compiling adder v0.1.0 (file:///projects/add/adder)

Run specific package:

$ cargo run -p adder
     Running `target/debug/adder`
Hello, world! 10 plus one is 11!

Test specific package:

$ cargo test -p add_one

External Dependencies

Dependencies must be explicitly added to each crate’s Cargo.toml:

# add_one/Cargo.toml
[dependencies]
rand = "0.8.5"

Workspace ensures all crates use the same version of shared dependencies.

Testing

Run all tests:

$ cargo test

Run specific crate tests:

$ cargo test -p add_one

Publishing

Each workspace member publishes independently:

$ cargo publish -p add_one
$ cargo publish -p adder

Benefits

  • Shared dependencies: Consistent versions across all crates
  • Unified build: Single command builds entire workspace
  • Cross-crate development: Easy local dependencies between related crates
  • Testing: Run tests across all crates simultaneously
  • Code sharing: Common utilities accessible to all workspace members

Workspaces are ideal for:

  • Multi-crate applications with shared libraries
  • Plugin architectures
  • Related tools that share common functionality

Installing Binaries with cargo install

Install command-line tools from crates.io for local development use.

Installation location: $HOME/.cargo/bin (ensure it’s in your $PATH)

Requirements: Crate must have binary targets (contains src/main.rs or specified binary)

Example: Installing ripgrep

$ cargo install ripgrep
    Updating crates.io index
  Downloaded ripgrep v14.1.1
  Installing ripgrep v14.1.1
   Compiling grep v0.3.2
    Finished `release` profile [optimized + debuginfo] target(s) in 6.73s
  Installing ~/.cargo/bin/rg
   Installed package `ripgrep v14.1.1` (executable `rg`)

The installed binary (rg) is immediately available if ~/.cargo/bin is in your PATH.

Common Use Cases

  • Development tools (cargo-watch, cargo-expand)
  • Text processing utilities (ripgrep, fd, bat)
  • System administration tools
  • Custom CLI applications from the Rust ecosystem

Note: cargo install is for developer tools, not system-wide package management.

Extending Cargo with Custom Commands

Cargo supports custom subcommands through executable naming convention.

Creating Custom Commands

Naming convention: cargo-<command-name> executable in $PATH

Usage: cargo <command-name> (Cargo finds and executes cargo-<command-name>)

Examples

Install extensions and use them as native Cargo commands:

$ cargo install cargo-watch
$ cargo watch  # Runs cargo-watch executable

List all available commands (including custom ones):

$ cargo --list

Benefits

  • Seamless integration with existing Cargo workflow
  • Discoverable through cargo --list
  • Standard installation via cargo install
  • Follows Unix tool composition principles

Custom commands enable domain-specific tooling while maintaining consistent user experience with core Cargo functionality.

Summary

Cargo’s ecosystem features enable productive Rust development:

  • Release profiles: Optimize builds for development vs. production
  • Publishing: Share code via crates.io with proper documentation
  • Workspaces: Manage complex multi-crate projects
  • Binary installation: Access community tools easily
  • Extensibility: Add custom functionality without modifying Cargo core

These features support the Rust ecosystem’s growth by making code sharing, discovery, and collaboration straightforward and reliable.

Smart Pointers

Smart pointers are data structures that act like pointers but provide additional metadata and capabilities beyond standard references. While references (&) only borrow values with no overhead, smart pointers often own their data and implement specific memory management patterns.

Key smart pointers in Rust’s standard library:

  • Box<T> - heap allocation with single ownership
  • Rc<T> - reference counting for multiple ownership (single-threaded)
  • Ref<T> and RefMut<T> via RefCell<T> - runtime borrow checking

Smart pointers implement the Deref and Drop traits:

  • Deref enables automatic dereferencing, allowing smart pointers to behave like references
  • Drop customizes cleanup behavior when values go out of scope

This chapter covers interior mutability patterns and reference cycle prevention using Weak<T>.

Core Concepts

Interior Mutability: Modifying data through immutable references using types like RefCell<T> that enforce borrowing rules at runtime rather than compile time.

Reference Cycles: Circular references that prevent memory cleanup, resolved using weak references (Weak<T>) that don’t affect reference counts.

Using Box<T> to Point to Data on the Heap

Box<T> allocates data on the heap with single ownership. Primary use cases:

  • Types with unknown compile-time size (recursive types)
  • Large data that should move without copying
  • Trait objects (covered in Chapter 18)

Basic Usage

fn main() {
    let b = Box::new(5);
    println!("b = {b}");
}

Box<T> provides heap allocation with automatic cleanup when it goes out of scope.

Enabling Recursive Types

Recursive types require indirection since Rust needs to know type sizes at compile time.

enum List {
    Cons(i32, List),
    Nil,
}

fn main() {}

This fails because List has infinite size:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0072]: recursive type `List` has infinite size
 --> src/main.rs:1:1
  |
1 | enum List {
  | ^^^^^^^^^
2 |     Cons(i32, List),
  |               ---- recursive without indirection
  |
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
  |
2 |     Cons(i32, Box<List>),
  |               ++++    +

error[E0391]: cycle detected when computing when `List` needs drop
 --> src/main.rs:1:1
  |
1 | enum List {
  | ^^^^^^^^^
  |
  = note: ...which immediately requires computing when `List` needs drop again
  = note: cycle used when computing whether `List` needs drop
  = note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information

Some errors have detailed explanations: E0072, E0391.
For more information about an error, try `rustc --explain E0072`.
error: could not compile `cons-list` (bin "cons-list") due to 2 previous errors

Size Calculation Issue

Non-recursive enums use the size of their largest variant. Recursive types create infinite size calculations since each Cons variant contains another List.

Solution with Box<T>

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}

Box<T> has a known size (pointer size), breaking the recursive chain. The heap-allocated data structure achieves the desired recursion without infinite compile-time size.

Box<T> implements Deref for reference-like behavior and Drop for automatic cleanup, making it a true smart pointer.

Treating Smart Pointers Like Regular References with Deref

The Deref trait customizes the dereference operator (*) behavior, allowing smart pointers to behave like regular references.

Basic Dereferencing

fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

Using Box<T> Like a Reference

fn main() {
    let x = 5;
    let y = Box::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

Box<T> can be dereferenced like a regular reference due to its Deref implementation.

Implementing Custom Smart Pointers

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn main() {}

This MyBox<T> type won’t work with the dereference operator without implementing Deref:

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn main() {
    let x = 5;
    let y = MyBox::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

Implementing the Deref Trait

use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn main() {
    let x = 5;
    let y = MyBox::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

The deref method returns a reference to the inner data. When you write *y, Rust actually executes *(y.deref()).

Deref Coercion

Deref coercion automatically converts references to types implementing Deref into references to other types. This enables seamless interaction between different smart pointer types.

fn hello(name: &str) {
    println!("Hello, {name}!");
}

fn main() {}

Calling a function expecting &str with &MyBox<String>:

use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn hello(name: &str) {
    println!("Hello, {name}!");
}

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&m);
}

Rust automatically applies deref coercion: &MyBox<String>&String&str.

Without deref coercion, you’d need explicit conversions:

use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn hello(name: &str) {
    println!("Hello, {name}!");
}

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&(*m)[..]);
}

Deref Coercion Rules

Rust performs deref coercion in three cases:

  1. &T to &U when T: Deref<Target=U>
  2. &mut T to &mut U when T: DerefMut<Target=U>
  3. &mut T to &U when T: Deref<Target=U>

Note: Immutable references cannot coerce to mutable references due to borrowing rules.

Running Code on Cleanup with the Drop Trait

The Drop trait customizes cleanup behavior when a value goes out of scope. It’s essential for smart pointers that manage resources like memory, files, or network connections.

Implementing Drop

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c = CustomSmartPointer {
        data: String::from("my stuff"),
    };
    let d = CustomSmartPointer {
        data: String::from("other stuff"),
    };
    println!("CustomSmartPointers created.");
}

The Drop trait requires implementing a drop method that takes &mut self. Rust automatically calls drop when values go out of scope.

Output:

$ cargo run
   Compiling drop-example v0.1.0 (file:///projects/drop-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.60s
     Running `target/debug/drop-example`
CustomSmartPointers created.
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!

Variables are dropped in reverse order of creation (d before c).

Manual Cleanup with std::mem::drop

You cannot call the Drop trait’s drop method directly:

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c = CustomSmartPointer {
        data: String::from("some data"),
    };
    println!("CustomSmartPointer created.");
    c.drop();
    println!("CustomSmartPointer dropped before the end of main.");
}
$ cargo run
   Compiling drop-example v0.1.0 (file:///projects/drop-example)
error[E0040]: explicit use of destructor method
  --> src/main.rs:16:7
   |
16 |     c.drop();
   |       ^^^^ explicit destructor calls not allowed
   |
help: consider using `drop` function
   |
16 |     drop(c);
   |     +++++ ~

For more information about this error, try `rustc --explain E0040`.
error: could not compile `drop-example` (bin "drop-example") due to 1 previous error

Use std::mem::drop for early cleanup:

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c = CustomSmartPointer {
        data: String::from("some data"),
    };
    println!("CustomSmartPointer created.");
    drop(c);
    println!("CustomSmartPointer dropped before the end of main.");
}

Output:

$ cargo run
   Compiling drop-example v0.1.0 (file:///projects/drop-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
     Running `target/debug/drop-example`
CustomSmartPointer created.
Dropping CustomSmartPointer with data `some data`!
CustomSmartPointer dropped before the end of main.

std::mem::drop takes ownership of the value, triggering the Drop implementation immediately. This prevents double-free errors since Rust can’t call drop again on a moved value.

Rc<T>, the Reference Counted Smart Pointer

Rc<T> (reference counting) enables multiple ownership of the same data by tracking reference counts. When the count reaches zero, the value is cleaned up. Single-threaded only.

Multiple Ownership Problem

Using Box<T> with multiple ownership fails:

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
    let b = Cons(3, Box::new(a));
    let c = Cons(4, Box::new(a));
}
$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0382]: use of moved value: `a`
  --> src/main.rs:11:30
   |
9  |     let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
   |         - move occurs because `a` has type `List`, which does not implement the `Copy` trait
10 |     let b = Cons(3, Box::new(a));
   |                              - value moved here
11 |     let c = Cons(4, Box::new(a));
   |                              ^ value used here after move

For more information about this error, try `rustc --explain E0382`.
error: could not compile `cons-list` (bin "cons-list") due to 1 previous error

Solution with Rc<T>

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a));
    let c = Cons(4, Rc::clone(&a));
}

Rc::clone increments the reference count without deep-copying data. Use Rc::clone instead of a.clone() to distinguish reference counting from expensive deep copies.

Reference Count Tracking

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("count after creating a = {}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a));
    println!("count after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, Rc::clone(&a));
        println!("count after creating c = {}", Rc::strong_count(&a));
    }
    println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}

Output:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.45s
     Running `target/debug/cons-list`
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

Rc::strong_count returns the current reference count. The Drop implementation automatically decrements the count when Rc<T> values go out of scope.

Rc<T> allows multiple readers but no mutable access. For mutable data with multiple owners, combine with RefCell<T> (covered next).

RefCell<T> and the Interior Mutability Pattern

Interior mutability allows mutating data through immutable references by deferring borrow checking to runtime. RefCell<T> enforces borrowing rules at runtime instead of compile time.

Runtime vs Compile-time Borrow Checking

TypeOwnershipBorrow CheckMutability
Box<T>SingleCompile-timeImmutable or mutable
Rc<T>MultipleCompile-timeImmutable only
RefCell<T>SingleRuntimeImmutable or mutable

RefCell<T> is single-threaded only. For multithreaded scenarios, use Mutex<T>.

Interior Mutability Use Case

Testing scenario requiring mutation through immutable interface:

pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
    T: Messenger,
{
    pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;

        if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger
                .send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.75 {
            self.messenger
                .send("Warning: You've used up over 75% of your quota!");
        }
    }
}

Attempting to implement a mock that tracks sent messages:

pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
    T: Messenger,
{
    pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;

        if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger
                .send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.75 {
            self.messenger
                .send("Warning: You've used up over 75% of your quota!");
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    struct MockMessenger {
        sent_messages: Vec<String>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger {
                sent_messages: vec![],
            }
        }
    }

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            self.sent_messages.push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        let mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

        limit_tracker.set_value(80);

        assert_eq!(mock_messenger.sent_messages.len(), 1);
    }
}

This fails because send takes &self but we need to mutate sent_messages:

$ cargo test
   Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
  --> src/lib.rs:58:13
   |
58 |             self.sent_messages.push(String::from(message));
   |             ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable
   |
help: consider changing this to be a mutable reference in the `impl` method and the `trait` definition
   |
2  ~     fn send(&mut self, msg: &str);
3  | }
...
56 |     impl Messenger for MockMessenger {
57 ~         fn send(&mut self, message: &str) {
   |

For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` (lib test) due to 1 previous error

Solution with RefCell<T>

pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
    T: Messenger,
{
    pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;

        if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger
                .send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.75 {
            self.messenger
                .send("Warning: You've used up over 75% of your quota!");
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;

    struct MockMessenger {
        sent_messages: RefCell<Vec<String>>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger {
                sent_messages: RefCell::new(vec![]),
            }
        }
    }

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            self.sent_messages.borrow_mut().push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        // --snip--
        let mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

        limit_tracker.set_value(80);

        assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
    }
}

RefCell<T> provides:

  • borrow() returns Ref<T> (immutable smart pointer)
  • borrow_mut() returns RefMut<T> (mutable smart pointer)

Both implement Deref for transparent access.

Runtime Borrow Checking

RefCell<T> tracks active borrows at runtime. Violating borrowing rules causes panics:

pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
    T: Messenger,
{
    pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;

        if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger
                .send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.75 {
            self.messenger
                .send("Warning: You've used up over 75% of your quota!");
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;

    struct MockMessenger {
        sent_messages: RefCell<Vec<String>>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger {
                sent_messages: RefCell::new(vec![]),
            }
        }
    }

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            let mut one_borrow = self.sent_messages.borrow_mut();
            let mut two_borrow = self.sent_messages.borrow_mut();

            one_borrow.push(String::from(message));
            two_borrow.push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        let mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

        limit_tracker.set_value(80);

        assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
    }
}
$ cargo test
   Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
     Running unittests src/lib.rs (target/debug/deps/limit_tracker-e599811fa246dbde)

running 1 test
test tests::it_sends_an_over_75_percent_warning_message ... FAILED

failures:

---- tests::it_sends_an_over_75_percent_warning_message stdout ----

thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at src/lib.rs:60:53:
already borrowed: BorrowMutError
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::it_sends_an_over_75_percent_warning_message

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

Combining Rc<T> and RefCell<T>

Multiple ownership with mutability:

#[derive(Debug)]
enum List {
    Cons(Rc<RefCell<i32>>, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let value = Rc::new(RefCell::new(5));

    let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));

    let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
    let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));

    *value.borrow_mut() += 10;

    println!("a after = {a:?}");
    println!("b after = {b:?}");
    println!("c after = {c:?}");
}

Output:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.63s
     Running `target/debug/cons-list`
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))

Rc<RefCell<T>> enables multiple owners who can all mutate the shared data, checked at runtime rather than compile time.

Reference Cycles Can Leak Memory

Rc<T> and RefCell<T> can create reference cycles where items refer to each other, preventing cleanup since reference counts never reach zero.

Creating a Reference Cycle

use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
enum List {
    Cons(i32, RefCell<Rc<List>>),
    Nil,
}

impl List {
    fn tail(&self) -> Option<&RefCell<Rc<List>>> {
        match self {
            Cons(_, item) => Some(item),
            Nil => None,
        }
    }
}

fn main() {}

Creating circular references:

use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
enum List {
    Cons(i32, RefCell<Rc<List>>),
    Nil,
}

impl List {
    fn tail(&self) -> Option<&RefCell<Rc<List>>> {
        match self {
            Cons(_, item) => Some(item),
            Nil => None,
        }
    }
}

fn main() {
    let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));

    println!("a initial rc count = {}", Rc::strong_count(&a));
    println!("a next item = {:?}", a.tail());

    let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));

    println!("a rc count after b creation = {}", Rc::strong_count(&a));
    println!("b initial rc count = {}", Rc::strong_count(&b));
    println!("b next item = {:?}", b.tail());

    if let Some(link) = a.tail() {
        *link.borrow_mut() = Rc::clone(&b);
    }

    println!("b rc count after changing a = {}", Rc::strong_count(&b));
    println!("a rc count after changing a = {}", Rc::strong_count(&a));

    // Uncomment the next line to see that we have a cycle;
    // it will overflow the stack.
    // println!("a next item = {:?}", a.tail());
}

Output:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.53s
     Running `target/debug/cons-list`
a initial rc count = 1
a next item = Some(RefCell { value: Nil })
a rc count after b creation = 2
b initial rc count = 1
b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
b rc count after changing a = 2
a rc count after changing a = 2

Both a and b have reference count 2. When they go out of scope, counts drop to 1 but never reach 0, causing a memory leak.

Preventing Reference Cycles Using Weak<T>

Weak<T> provides non-owning references that don’t affect reference counts.

  • Rc::downgrade creates Weak<T> from Rc<T>
  • Weak::upgrade returns Option<Rc<T>>
  • Strong references control cleanup; weak references don’t

Tree Data Structure Example

use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Node {
    value: i32,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        children: RefCell::new(vec![]),
    });

    let branch = Rc::new(Node {
        value: 5,
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });
}
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Node {
    value: i32,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        children: RefCell::new(vec![]),
    });

    let branch = Rc::new(Node {
        value: 5,
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });
}

Adding parent references with Weak<T>:

use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());

    let branch = Rc::new(Node {
        value: 5,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });

    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}
use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());

    let branch = Rc::new(Node {
        value: 5,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });

    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}

Reference Count Monitoring

use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    println!(
        "leaf strong = {}, weak = {}",
        Rc::strong_count(&leaf),
        Rc::weak_count(&leaf),
    );

    {
        let branch = Rc::new(Node {
            value: 5,
            parent: RefCell::new(Weak::new()),
            children: RefCell::new(vec![Rc::clone(&leaf)]),
        });

        *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

        println!(
            "branch strong = {}, weak = {}",
            Rc::strong_count(&branch),
            Rc::weak_count(&branch),
        );

        println!(
            "leaf strong = {}, weak = {}",
            Rc::strong_count(&leaf),
            Rc::weak_count(&leaf),
        );
    }

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
    println!(
        "leaf strong = {}, weak = {}",
        Rc::strong_count(&leaf),
        Rc::weak_count(&leaf),
    );
}
  • Rc::strong_count tracks owning references
  • Rc::weak_count tracks non-owning references
  • Only strong count reaching zero triggers cleanup

When branch goes out of scope, its strong count drops to 0 and gets cleaned up despite having weak references pointing to it.

Summary

Smart pointers provide different memory management patterns:

  • Box<T>: Single ownership, heap allocation
  • Rc<T>: Multiple ownership via reference counting
  • RefCell<T>: Interior mutability with runtime borrow checking
  • Weak<T>: Non-owning references to prevent cycles

Use Weak<T> for parent-child relationships where children should not own their parents, preventing reference cycles and memory leaks.

Fearless Concurrency

Rust’s ownership and type systems provide compile-time guarantees for concurrent programming. Unlike runtime concurrency bugs common in other languages, Rust’s approach moves many concurrency errors to compile time, eliminating data races and memory safety issues through static analysis.

Rust supports multiple concurrency paradigms:

  • Message-passing concurrency: Channels for thread communication
  • Shared-state concurrency: Mutexes and atomic types for shared data access
  • Thread creation: OS threads with the thread::spawn API
  • Send/Sync traits: Compile-time thread safety guarantees

The standard library provides a 1:1 threading model (one OS thread per language thread), with async/await as an alternative concurrency model covered in the next chapter.

This chapter covers:

  • Thread creation and management
  • Channel-based message passing
  • Shared state with mutexes and atomic reference counting
  • The Send and Sync traits for extensible concurrency

Using Threads to Run Code Simultaneously

Rust uses a 1:1 threading model where each language thread maps to an OS thread. Thread creation uses thread::spawn with closures containing the code to execute.

Creating Threads with spawn

use std::thread;
use std::time::Duration;

fn main() {
    thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {i} from the spawned thread!");
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("hi number {i} from the main thread!");
        thread::sleep(Duration::from_millis(1));
    }
}

Key behaviors:

  • Main thread completion terminates all spawned threads
  • Thread execution order is non-deterministic
  • thread::sleep yields execution to other threads

Waiting for Thread Completion with join

thread::spawn returns a JoinHandle<T> that provides a join method to wait for thread completion:

use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {i} from the spawned thread!");
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("hi number {i} from the main thread!");
        thread::sleep(Duration::from_millis(1));
    }

    handle.join().unwrap();
}

Calling join blocks the current thread until the target thread terminates. Placement of join calls affects concurrency behavior—early joins eliminate parallelism.

Using move Closures for Data Transfer

To transfer ownership of variables from one thread to another, use move closures:

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(|| {
        println!("Here's a vector: {v:?}");
    });

    handle.join().unwrap();
}

This fails because Rust cannot guarantee the lifetime of borrowed data across thread boundaries:

$ cargo run
   Compiling threads v0.1.0 (file:///projects/threads)
error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function
 --> src/main.rs:6:32
  |
6 |     let handle = thread::spawn(|| {
  |                                ^^ may outlive borrowed value `v`
7 |         println!("Here's a vector: {v:?}");
  |                                     - `v` is borrowed here
  |
note: function requires argument type to outlive `'static`
 --> src/main.rs:6:18
  |
6 |       let handle = thread::spawn(|| {
  |  __________________^
7 | |         println!("Here's a vector: {v:?}");
8 | |     });
  | |______^
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
  |
6 |     let handle = thread::spawn(move || {
  |                                ++++

For more information about this error, try `rustc --explain E0373`.
error: could not compile `threads` (bin "threads") due to 1 previous error

The move keyword forces the closure to take ownership:

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(move || {
        println!("Here's a vector: {v:?}");
    });

    handle.join().unwrap();
}

This transfers ownership to the spawned thread, preventing use-after-free errors and ensuring thread safety through compile-time checks.

Using Message Passing to Transfer Data Between Threads

Message passing enables thread communication by sending data through channels. Rust implements channels via std::sync::mpsc (multiple producer, single consumer).

Channel Basics

Channels provide a transmitter/receiver pair for unidirectional data flow:

use std::sync::mpsc;

fn main() {
    let (tx, rx) = mpsc::channel();
}

mpsc::channel() returns (Sender<T>, Receiver<T>). The mpsc design allows multiple senders but only one receiver.

Sending Data Between Threads

use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let val = String::from("hi");
        tx.send(val).unwrap();
    });
}

The send method returns Result<(), SendError<T>>. It fails when the receiver is dropped.

use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let val = String::from("hi");
        tx.send(val).unwrap();
    });

    let received = rx.recv().unwrap();
    println!("Got: {received}");
}

Receiver methods:

  • recv(): Blocks until a value arrives, returns Result<T, RecvError>
  • try_recv(): Non-blocking, returns Result<T, TryRecvError> immediately

Channels and Ownership

Channels transfer ownership of sent values, preventing use-after-send errors:

use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let val = String::from("hi");
        tx.send(val).unwrap();
        println!("val is {val}");
    });

    let received = rx.recv().unwrap();
    println!("Got: {received}");
}

Compilation fails with:

$ cargo run
   Compiling message-passing v0.1.0 (file:///projects/message-passing)
error[E0382]: borrow of moved value: `val`
  --> src/main.rs:10:26
   |
8  |         let val = String::from("hi");
   |             --- move occurs because `val` has type `String`, which does not implement the `Copy` trait
9  |         tx.send(val).unwrap();
   |                 --- value moved here
10 |         println!("val is {val}");
   |                          ^^^^^ value borrowed here after move
   |
   = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0382`.
error: could not compile `message-passing` (bin "message-passing") due to 1 previous error

The send operation moves ownership to the receiver, preventing data races.

Multiple Values and Iterator Pattern

use std::sync::mpsc;
use std::thread;
use std::time::Duration;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let vals = vec![
            String::from("hi"),
            String::from("from"),
            String::from("the"),
            String::from("thread"),
        ];

        for val in vals {
            tx.send(val).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });

    for received in rx {
        println!("Got: {received}");
    }
}

The receiver implements Iterator, terminating when the channel closes (all senders dropped).

Multiple Producers

Clone the transmitter to create multiple senders:

use std::sync::mpsc;
use std::thread;
use std::time::Duration;

fn main() {
    // --snip--

    let (tx, rx) = mpsc::channel();

    let tx1 = tx.clone();
    thread::spawn(move || {
        let vals = vec![
            String::from("hi"),
            String::from("from"),
            String::from("the"),
            String::from("thread"),
        ];

        for val in vals {
            tx1.send(val).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });

    thread::spawn(move || {
        let vals = vec![
            String::from("more"),
            String::from("messages"),
            String::from("for"),
            String::from("you"),
        ];

        for val in vals {
            tx.send(val).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });

    for received in rx {
        println!("Got: {received}");
    }

    // --snip--
}

Each cloned sender can send independently. Message order depends on thread scheduling and is non-deterministic.

Shared-State Concurrency

Unlike message passing’s single ownership model, shared-state concurrency allows multiple threads to access the same memory locations simultaneously. Rust provides thread-safe primitives through mutexes and atomic reference counting.

Mutexes for Exclusive Data Access

A mutex (mutual exclusion) guards data with a locking mechanism, ensuring only one thread accesses the data at a time.

use std::sync::Mutex;

fn main() {
    let m = Mutex::new(5);

    {
        let mut num = m.lock().unwrap();
        *num = 6;
    }

    println!("m = {m:?}");
}

Key Mutex<T> characteristics:

  • lock() returns LockResult<MutexGuard<T>>
  • MutexGuard<T> implements Deref to access inner data
  • Drop implementation automatically releases the lock when guard goes out of scope
  • Lock acquisition blocks until available or panics if poisoned

Sharing Mutexes Between Threads

Attempting to share a mutex across threads fails without proper ownership handling:

use std::sync::Mutex;
use std::thread;

fn main() {
    let counter = Mutex::new(0);
    let mut handles = vec![];

    for _ in 0..10 {
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();

            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}

Error: cannot move counter into multiple threads:

$ cargo run
   Compiling shared-state v0.1.0 (file:///projects/shared-state)
error[E0382]: borrow of moved value: `counter`
  --> src/main.rs:21:29
   |
5  |     let counter = Mutex::new(0);
   |         ------- move occurs because `counter` has type `Mutex<i32>`, which does not implement the `Copy` trait
...
8  |     for _ in 0..10 {
   |     -------------- inside of this loop
9  |         let handle = thread::spawn(move || {
   |                                    ------- value moved into closure here, in previous iteration of loop
...
21 |     println!("Result: {}", *counter.lock().unwrap());
   |                             ^^^^^^^ value borrowed here after move
   |
help: consider moving the expression out of the loop so it is only moved once
   |
8  ~     let mut value = counter.lock();
9  ~     for _ in 0..10 {
10 |         let handle = thread::spawn(move || {
11 ~             let mut num = value.unwrap();
   |

For more information about this error, try `rustc --explain E0382`.
error: could not compile `shared-state` (bin "shared-state") due to 1 previous error

Multiple Ownership with Rc<T> Limitation

Rc<T> provides multiple ownership but is not thread-safe:

use std::rc::Rc;
use std::sync::Mutex;
use std::thread;

fn main() {
    let counter = Rc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Rc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();

            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}

Error: Rc<T> lacks Send trait for thread safety:

$ cargo run
   Compiling shared-state v0.1.0 (file:///projects/shared-state)
error[E0277]: `Rc<Mutex<i32>>` cannot be sent between threads safely
  --> src/main.rs:11:36
   |
11 |           let handle = thread::spawn(move || {
   |                        ------------- ^------
   |                        |             |
   |  ______________________|_____________within this `{closure@src/main.rs:11:36: 11:43}`
   | |                      |
   | |                      required by a bound introduced by this call
12 | |             let mut num = counter.lock().unwrap();
13 | |
14 | |             *num += 1;
15 | |         });
   | |_________^ `Rc<Mutex<i32>>` cannot be sent between threads safely
   |
   = help: within `{closure@src/main.rs:11:36: 11:43}`, the trait `Send` is not implemented for `Rc<Mutex<i32>>`
note: required because it's used within this closure
  --> src/main.rs:11:36
   |
11 |         let handle = thread::spawn(move || {
   |                                    ^^^^^^^
note: required by a bound in `spawn`
  --> /rustc/4eb161250e340c8f48f66e2b929ef4a5bed7c181/library/std/src/thread/mod.rs:728:1

For more information about this error, try `rustc --explain E0277`.
error: could not compile `shared-state` (bin "shared-state") due to 1 previous error

Rc<T> uses non-atomic reference counting, making it unsafe for concurrent access.

Atomic Reference Counting with Arc<T>

Arc<T> (atomically reference-counted) provides thread-safe multiple ownership:

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();

            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}

Arc<T> uses atomic operations for reference counting, ensuring thread safety. Atomic operations have performance overhead, so use only when needed for concurrent access.

Interior Mutability Pattern

Mutex<T> provides interior mutability similar to RefCell<T>:

  • RefCell<T>/Rc<T>: Single-threaded shared ownership with runtime borrow checking
  • Mutex<T>/Arc<T>: Multi-threaded shared ownership with locking

Both patterns allow mutation through immutable references, but Mutex<T> includes deadlock risks when acquiring multiple locks.

Extensible Concurrency with the Send and Sync Traits

Rust’s concurrency safety is enforced through two marker traits: Send and Sync. These traits are implemented automatically for most types but can be manually implemented for custom concurrent types.

The Send Trait

Send indicates that ownership of a type can be transferred between threads safely. Almost all Rust types implement Send, with notable exceptions:

  • Rc<T>: Not Send because non-atomic reference counting creates race conditions
  • Raw pointers: Lack safety guarantees

Types composed entirely of Send types automatically implement Send. When we attempted to send Rc<Mutex<i32>> between threads in Listing 16-14, the compiler prevented this due to Rc<T>’s lack of Send implementation.

The Sync Trait

Sync indicates that a type is safe to access from multiple threads simultaneously. A type T implements Sync if &T (an immutable reference to T) implements Send.

Types that don’t implement Sync:

  • Rc<T>: Non-atomic reference counting unsafe for concurrent access
  • RefCell<T> and Cell<T>: Runtime borrow checking not thread-safe
  • Mutex<T>: Implements Sync and enables multi-threaded shared access

Primitive types implement both Send and Sync. Types composed entirely of Send and Sync types automatically implement these traits.

Manual Implementation Safety

Manually implementing Send and Sync requires unsafe code and careful consideration of concurrency invariants. Building new concurrent types outside the Send/Sync ecosystem demands deep understanding of thread safety guarantees.

For production systems, prefer established concurrent data structures from crates like crossbeam or tokio rather than implementing custom concurrent types.

Summary

Rust’s concurrency model combines:

  • Channels: Message passing with ownership transfer
  • Mutexes: Shared state with exclusive access
  • Atomic types: Lock-free concurrent operations
  • Send/Sync traits: Compile-time thread safety guarantees

The type system and borrow checker eliminate data races and memory safety issues at compile time, enabling fearless concurrency. Concurrent programming becomes a design decision rather than a source of runtime bugs.

Asynchronous Programming: Async, Await, Futures, and Streams

Modern applications require handling concurrent operations efficiently. Rust provides async programming through Futures, Streams, and the async/await syntax. This chapter covers async fundamentals and practical patterns for building concurrent systems.

You’ll learn:

  • async/await syntax and the Future trait
  • Concurrent programming patterns with async
  • Working with multiple futures and streams
  • Trait details: Future, Pin, Unpin, Stream
  • Combining async with threads

Parallelism vs Concurrency

Concurrency: Single worker switches between tasks before completion (time-slicing). Parallelism: Multiple workers execute tasks simultaneously. Serial: Tasks must complete in sequence due to dependencies.

Concurrent:  A1 → B1 → A2 → B2 → A3 → B3
Parallel:    A1 → A2 → A3
             B1 → B2 → B3

CPU-bound operations benefit from parallelism. IO-bound operations benefit from concurrency. Most real systems need both.

In async Rust, concurrency is primary. The runtime may use parallelism underneath, but your async code provides concurrency semantics.

Blocking vs Non-blocking

Traditional IO operations block execution until completion. Async operations yield control to the runtime when waiting, allowing other tasks to progress.

// Blocking - thread stops here
let data = std::fs::read("file.txt")?;

// Non-blocking - runtime can schedule other work
let data = tokio::fs::read("file.txt").await?;

Async enables writing sequential-looking code that’s actually concurrent:

let data = fetch_data_from(url).await;
println!("{data}");

This syntax compiles to a state machine that yields control at await points.

Futures and the Async Syntax

Rust’s async programming centers on futures - values that represent work that may complete later. The Future trait provides a common interface for async operations, similar to Promises in JavaScript.

Key concepts:

  • async functions/blocks return futures
  • await polls futures to completion
  • Futures are lazy - they do nothing until awaited
  • Each await point is where execution can pause/resume
async fn fetch_title(url: &str) -> Option<String> {
    let response = http_get(url).await;  // Yield control here
    let html = response.text().await;    // And here
    parse_title(&html)
}

Setup

Create a new project and add the trpl crate, which provides runtime and utility functions:

$ cargo new hello-async && cd hello-async
$ cargo add trpl

First Async Program: Web Scraper

extern crate trpl; // required for mdbook test

fn main() {
    // TODO: we'll add this next!
}

use trpl::Html;

async fn page_title(url: &str) -> Option<String> {
    let response = trpl::get(url).await;
    let response_text = response.text().await;
    Html::parse(&response_text)
        .select_first("title")
        .map(|title_element| title_element.inner_html())
}

Key points:

  • async fn returns impl Future<Output = ReturnType>
  • await is postfix: future.await not await future
  • Network and parsing operations yield control at await points
  • Chaining with await creates readable async code:
extern crate trpl; // required for mdbook test

use trpl::Html;

fn main() {
    // TODO: we'll add this next!
}

async fn page_title(url: &str) -> Option<String> {
    let response_text = trpl::get(url).await.text().await;
    Html::parse(&response_text)
        .select_first("title")
        .map(|title_element| title_element.inner_html())
}

Async Function Compilation

The compiler transforms:

async fn page_title(url: &str) -> Option<String> { /* body */ }

Into roughly:

fn page_title(url: &str) -> impl Future<Output = Option<String>> {
    async move { /* body */ }
}

Running Async Code

main cannot be async - you need a runtime to execute futures:

extern crate trpl; // required for mdbook test

use trpl::Html;

async fn main() {
    let args: Vec<String> = std::env::args().collect();
    let url = &args[1];
    match page_title(url).await {
        Some(title) => println!("The title for {url} was {title}"),
        None => println!("{url} had no title"),
    }
}

async fn page_title(url: &str) -> Option<String> {
    let response_text = trpl::get(url).await.text().await;
    Html::parse(&response_text)
        .select_first("title")
        .map(|title_element| title_element.inner_html())
}

Fix with trpl::run:

extern crate trpl; // required for mdbook test

use trpl::Html;

fn main() {
    let args: Vec<String> = std::env::args().collect();

    trpl::run(async {
        let url = &args[1];
        match page_title(url).await {
            Some(title) => println!("The title for {url} was {title}"),
            None => println!("{url} had no title"),
        }
    })
}

async fn page_title(url: &str) -> Option<String> {
    let response_text = trpl::get(url).await.text().await;
    Html::parse(&response_text)
        .select_first("title")
        .map(|title_element| title_element.inner_html())
}

The runtime manages the state machine that Rust creates from async blocks. Each await point represents where the state machine can pause and resume.

Racing Multiple Futures

extern crate trpl; // required for mdbook test

use trpl::{Either, Html};

fn main() {
    let args: Vec<String> = std::env::args().collect();

    trpl::run(async {
        let title_fut_1 = page_title(&args[1]);
        let title_fut_2 = page_title(&args[2]);

        let (url, maybe_title) =
            match trpl::race(title_fut_1, title_fut_2).await {
                Either::Left(left) => left,
                Either::Right(right) => right,
            };

        println!("{url} returned first");
        match maybe_title {
            Some(title) => println!("Its page title is: '{title}'"),
            None => println!("Its title could not be parsed."),
        }
    })
}

async fn page_title(url: &str) -> (&str, Option<String>) {
    let text = trpl::get(url).await.text().await;
    let title = Html::parse(&text)
        .select_first("title")
        .map(|title| title.inner_html());
    (url, title)
}

trpl::race returns Either<Left(A), Right(B)> indicating which future completed first. Unlike threads, the order of argument evaluation affects which future starts first (non-fair scheduling).

Runtime and State Machines

Async blocks compile to state machines. The runtime (executor) manages these machines:

enum PageTitleState {
    Start,
    WaitingForResponse,
    WaitingForText,
    Complete,
}

At each await point, the future yields control back to the runtime. The runtime can then:

  • Schedule other futures
  • Handle IO completion
  • Resume futures when their dependencies are ready

This cooperative multitasking requires explicit yield points (await) - CPU-intensive work without await points will block other futures.

Concurrency with Async

Async provides different APIs and performance characteristics compared to threads, but many patterns are similar. Key differences:

  • Tasks are lightweight compared to OS threads
  • Fair vs unfair scheduling
  • Cooperative vs preemptive multitasking

Spawning Tasks

extern crate trpl; // required for mdbook test

use std::time::Duration;

fn main() {
    trpl::run(async {
        trpl::spawn_task(async {
            for i in 1..10 {
                println!("hi number {i} from the first task!");
                trpl::sleep(Duration::from_millis(500)).await;
            }
        });

        for i in 1..5 {
            println!("hi number {i} from the second task!");
            trpl::sleep(Duration::from_millis(500)).await;
        }
    });
}

Like thread::spawn, trpl::spawn_task runs code concurrently. Unlike threads:

  • Tasks run on the same thread (unless using a multi-threaded runtime)
  • Much lower overhead - millions of tasks are feasible
  • Terminated when the runtime shuts down

Joining Tasks

Tasks return join handles that implement Future:

extern crate trpl; // required for mdbook test

use std::time::Duration;

fn main() {
    trpl::run(async {
        let handle = trpl::spawn_task(async {
            for i in 1..10 {
                println!("hi number {i} from the first task!");
                trpl::sleep(Duration::from_millis(500)).await;
            }
        });

        for i in 1..5 {
            println!("hi number {i} from the second task!");
            trpl::sleep(Duration::from_millis(500)).await;
        }

        handle.await.unwrap();
    });
}

Joining Multiple Futures

trpl::join combines futures and waits for all to complete:

extern crate trpl; // required for mdbook test

use std::time::Duration;

fn main() {
    trpl::run(async {
        let fut1 = async {
            for i in 1..10 {
                println!("hi number {i} from the first task!");
                trpl::sleep(Duration::from_millis(500)).await;
            }
        };

        let fut2 = async {
            for i in 1..5 {
                println!("hi number {i} from the second task!");
                trpl::sleep(Duration::from_millis(500)).await;
            }
        };

        trpl::join(fut1, fut2).await;
    });
}

Key points about join:

  • Fair scheduling: alternates between futures equally
  • Deterministic ordering: consistent execution pattern
  • Structured concurrency: all futures complete before continuing

Compare this to threads where the OS scheduler determines execution order non-deterministically.

Message Passing

Async channels work similarly to sync channels but with async methods:

extern crate trpl; // required for mdbook test

fn main() {
    trpl::run(async {
        let (tx, mut rx) = trpl::channel();

        let val = String::from("hi");
        tx.send(val).unwrap();

        let received = rx.recv().await.unwrap();
        println!("Got: {received}");
    });
}

The recv method returns a Future that resolves when a message arrives.

Multiple Messages with Delays

extern crate trpl; // required for mdbook test

use std::time::Duration;

fn main() {
    trpl::run(async {
        let (tx, mut rx) = trpl::channel();

        let vals = vec![
            String::from("hi"),
            String::from("from"),
            String::from("the"),
            String::from("future"),
        ];

        for val in vals {
            tx.send(val).unwrap();
            trpl::sleep(Duration::from_millis(500)).await;
        }

        while let Some(value) = rx.recv().await {
            println!("received '{value}'");
        }
    });
}

This runs serially - all sends complete before receives start. For concurrency, separate into different futures:

extern crate trpl; // required for mdbook test

use std::time::Duration;

fn main() {
    trpl::run(async {
        let (tx, mut rx) = trpl::channel();

        let tx_fut = async {
            let vals = vec![
                String::from("hi"),
                String::from("from"),
                String::from("the"),
                String::from("future"),
            ];

            for val in vals {
                tx.send(val).unwrap();
                trpl::sleep(Duration::from_millis(500)).await;
            }
        };

        let rx_fut = async {
            while let Some(value) = rx.recv().await {
                println!("received '{value}'");
            }
        };

        trpl::join(tx_fut, rx_fut).await;
    });
}

Channel Cleanup

Channels need proper cleanup to avoid infinite loops. Move the sender into an async block so it gets dropped when sending completes:

extern crate trpl; // required for mdbook test

use std::time::Duration;

fn main() {
    trpl::run(async {
        let (tx, mut rx) = trpl::channel();

        let tx_fut = async move {
            let vals = vec![
                String::from("hi"),
                String::from("from"),
                String::from("the"),
                String::from("future"),
            ];

            for val in vals {
                tx.send(val).unwrap();
                trpl::sleep(Duration::from_millis(500)).await;
            }
        };

        let rx_fut = async {
            while let Some(value) = rx.recv().await {
                println!("received '{value}'");
            }
        };

        trpl::join(tx_fut, rx_fut).await;
    });
}

The move keyword transfers ownership into the async block. When the block completes, tx is dropped, closing the channel.

Multiple Producers

Clone the sender for multiple producers:

extern crate trpl; // required for mdbook test

use std::time::Duration;

fn main() {
    trpl::run(async {
        let (tx, mut rx) = trpl::channel();

        let tx1 = tx.clone();
        let tx1_fut = async move {
            let vals = vec![
                String::from("hi"),
                String::from("from"),
                String::from("the"),
                String::from("future"),
            ];

            for val in vals {
                tx1.send(val).unwrap();
                trpl::sleep(Duration::from_millis(500)).await;
            }
        };

        let rx_fut = async {
            while let Some(value) = rx.recv().await {
                println!("received '{value}'");
            }
        };

        let tx_fut = async move {
            let vals = vec![
                String::from("more"),
                String::from("messages"),
                String::from("for"),
                String::from("you"),
            ];

            for val in vals {
                tx.send(val).unwrap();
                trpl::sleep(Duration::from_millis(1500)).await;
            }
        };

        trpl::join3(tx1_fut, tx_fut, rx_fut).await;
    });
}

Use trpl::join3 for three futures, or the join! macro for variable numbers.

Patterns Summary

Task Spawning: spawn_task for fire-and-forget concurrency Joining: join/join! for structured concurrency waiting on all futures Channels: Async message passing with trpl::channel Cleanup: Use async move to ensure proper resource cleanup

Working with Any Number of Futures

Variable-Arity Joins

The join! macro handles variable numbers of futures:

extern crate trpl; // required for mdbook test

use std::time::Duration;

fn main() {
    trpl::run(async {
        let (tx, mut rx) = trpl::channel();

        let tx1 = tx.clone();
        let tx1_fut = async move {
            let vals = vec![
                String::from("hi"),
                String::from("from"),
                String::from("the"),
                String::from("future"),
            ];

            for val in vals {
                tx1.send(val).unwrap();
                trpl::sleep(Duration::from_secs(1)).await;
            }
        };

        let rx_fut = async {
            while let Some(value) = rx.recv().await {
                println!("received '{value}'");
            }
        };

        let tx_fut = async move {
            let vals = vec![
                String::from("more"),
                String::from("messages"),
                String::from("for"),
                String::from("you"),
            ];

            for val in vals {
                tx.send(val).unwrap();
                trpl::sleep(Duration::from_secs(1)).await;
            }
        };

        trpl::join!(tx1_fut, tx_fut, rx_fut);
    });
}

For dynamic collections of futures, use join_all:

extern crate trpl; // required for mdbook test

use std::time::Duration;

fn main() {
    trpl::run(async {
        let (tx, mut rx) = trpl::channel();

        let tx1 = tx.clone();
        let tx1_fut = async move {
            let vals = vec![
                String::from("hi"),
                String::from("from"),
                String::from("the"),
                String::from("future"),
            ];

            for val in vals {
                tx1.send(val).unwrap();
                trpl::sleep(Duration::from_secs(1)).await;
            }
        };

        let rx_fut = async {
            while let Some(value) = rx.recv().await {
                println!("received '{value}'");
            }
        };

        let tx_fut = async move {
            let vals = vec![
                String::from("more"),
                String::from("messages"),
                String::from("for"),
                String::from("you"),
            ];

            for val in vals {
                tx.send(val).unwrap();
                trpl::sleep(Duration::from_secs(1)).await;
            }
        };

        let futures = vec![tx1_fut, rx_fut, tx_fut];

        trpl::join_all(futures).await;
    });
}

Error: Each async block creates a unique anonymous type. Even identical blocks have different types.

Trait Objects for Dynamic Collections

Use trait objects to unify future types:

extern crate trpl; // required for mdbook test

use std::time::Duration;

fn main() {
    trpl::run(async {
        let (tx, mut rx) = trpl::channel();

        let tx1 = tx.clone();
        let tx1_fut = async move {
            let vals = vec![
                String::from("hi"),
                String::from("from"),
                String::from("the"),
                String::from("future"),
            ];

            for val in vals {
                tx1.send(val).unwrap();
                trpl::sleep(Duration::from_secs(1)).await;
            }
        };

        let rx_fut = async {
            while let Some(value) = rx.recv().await {
                println!("received '{value}'");
            }
        };

        let tx_fut = async move {
            let vals = vec![
                String::from("more"),
                String::from("messages"),
                String::from("for"),
                String::from("you"),
            ];

            for val in vals {
                tx.send(val).unwrap();
                trpl::sleep(Duration::from_secs(1)).await;
            }
        };

        let futures =
            vec![Box::new(tx1_fut), Box::new(rx_fut), Box::new(tx_fut)];

        trpl::join_all(futures).await;
    });
}
extern crate trpl; // required for mdbook test

use std::time::Duration;

fn main() {
    trpl::run(async {
        let (tx, mut rx) = trpl::channel();

        let tx1 = tx.clone();
        let tx1_fut = async move {
            let vals = vec![
                String::from("hi"),
                String::from("from"),
                String::from("the"),
                String::from("future"),
            ];

            for val in vals {
                tx1.send(val).unwrap();
                trpl::sleep(Duration::from_secs(1)).await;
            }
        };

        let rx_fut = async {
            while let Some(value) = rx.recv().await {
                println!("received '{value}'");
            }
        };

        let tx_fut = async move {
            let vals = vec![
                String::from("more"),
                String::from("messages"),
                String::from("for"),
                String::from("you"),
            ];

            for val in vals {
                tx.send(val).unwrap();
                trpl::sleep(Duration::from_secs(1)).await;
            }
        };

        let futures: Vec<Box<dyn Future<Output = ()>>> =
            vec![Box::new(tx1_fut), Box::new(rx_fut), Box::new(tx_fut)];

        trpl::join_all(futures).await;
    });
}

Pin Requirements: join_all requires futures to implement Unpin. Most async blocks don’t implement Unpin automatically.

Pinning Futures

Use Pin<Box<T>> to satisfy the Unpin requirement:

extern crate trpl; // required for mdbook test

use std::pin::Pin;

// -- snip --

use std::time::Duration;

fn main() {
    trpl::run(async {
        let (tx, mut rx) = trpl::channel();

        let tx1 = tx.clone();
        let tx1_fut = async move {
            let vals = vec![
                String::from("hi"),
                String::from("from"),
                String::from("the"),
                String::from("future"),
            ];

            for val in vals {
                tx1.send(val).unwrap();
                trpl::sleep(Duration::from_secs(1)).await;
            }
        };

        let rx_fut = async {
            while let Some(value) = rx.recv().await {
                println!("received '{value}'");
            }
        };

        let tx_fut = async move {
            let vals = vec![
                String::from("more"),
                String::from("messages"),
                String::from("for"),
                String::from("you"),
            ];

            for val in vals {
                tx.send(val).unwrap();
                trpl::sleep(Duration::from_secs(1)).await;
            }
        };

        let futures: Vec<Pin<Box<dyn Future<Output = ()>>>> =
            vec![Box::pin(tx1_fut), Box::pin(rx_fut), Box::pin(tx_fut)];

        trpl::join_all(futures).await;
    });
}

Performance optimization: Use pin! macro to avoid heap allocation:

extern crate trpl; // required for mdbook test

use std::pin::{Pin, pin};

// -- snip --

use std::time::Duration;

fn main() {
    trpl::run(async {
        let (tx, mut rx) = trpl::channel();

        let tx1 = tx.clone();
        let tx1_fut = pin!(async move {
            // --snip--
            let vals = vec![
                String::from("hi"),
                String::from("from"),
                String::from("the"),
                String::from("future"),
            ];

            for val in vals {
                tx1.send(val).unwrap();
                trpl::sleep(Duration::from_secs(1)).await;
            }
        });

        let rx_fut = pin!(async {
            // --snip--
            while let Some(value) = rx.recv().await {
                println!("received '{value}'");
            }
        });

        let tx_fut = pin!(async move {
            // --snip--
            let vals = vec![
                String::from("more"),
                String::from("messages"),
                String::from("for"),
                String::from("you"),
            ];

            for val in vals {
                tx.send(val).unwrap();
                trpl::sleep(Duration::from_secs(1)).await;
            }
        });

        let futures: Vec<Pin<&mut dyn Future<Output = ()>>> =
            vec![tx1_fut, rx_fut, tx_fut];

        trpl::join_all(futures).await;
    });
}

Tradeoffs

Same types + dynamic count: Use join_all with trait objects and pinning Different types + known count: Use join! macro or specific join functions Performance: Stack pinning (pin!) vs heap allocation (Box::pin)

extern crate trpl; // required for mdbook test

fn main() {
    trpl::run(async {
        let a = async { 1u32 };
        let b = async { "Hello!" };
        let c = async { true };

        let (a_result, b_result, c_result) = trpl::join!(a, b, c);
        println!("{a_result}, {b_result}, {c_result}");
    });
}

The join! macro handles heterogeneous types but requires compile-time knowledge of count and types.

Racing Futures

race returns the first completed future, discarding others:

extern crate trpl; // required for mdbook test

use std::time::Duration;

fn main() {
    trpl::run(async {
        let slow = async {
            println!("'slow' started.");
            trpl::sleep(Duration::from_millis(100)).await;
            println!("'slow' finished.");
        };

        let fast = async {
            println!("'fast' started.");
            trpl::sleep(Duration::from_millis(50)).await;
            println!("'fast' finished.");
        };

        trpl::race(slow, fast).await;
    });
}

Key point: Futures only yield at await points. CPU-intensive work without await points will block other futures (cooperative multitasking).

Yielding Control

Force yield points with yield_now():

extern crate trpl; // required for mdbook test

use std::{thread, time::Duration};

fn main() {
    trpl::run(async {
        // We will call `slow` here later
    });
}

fn slow(name: &str, ms: u64) {
    thread::sleep(Duration::from_millis(ms));
    println!("'{name}' ran for {ms}ms");
}
extern crate trpl; // required for mdbook test

use std::{thread, time::Duration};

fn main() {
    trpl::run(async {
        let a = async {
            println!("'a' started.");
            slow("a", 30);
            slow("a", 10);
            slow("a", 20);
            trpl::sleep(Duration::from_millis(50)).await;
            println!("'a' finished.");
        };

        let b = async {
            println!("'b' started.");
            slow("b", 75);
            slow("b", 10);
            slow("b", 15);
            slow("b", 350);
            trpl::sleep(Duration::from_millis(50)).await;
            println!("'b' finished.");
        };

        trpl::race(a, b).await;
    });
}

fn slow(name: &str, ms: u64) {
    thread::sleep(Duration::from_millis(ms));
    println!("'{name}' ran for {ms}ms");
}
extern crate trpl; // required for mdbook test

use std::{thread, time::Duration};

fn main() {
    trpl::run(async {
        let one_ms = Duration::from_millis(1);

        let a = async {
            println!("'a' started.");
            slow("a", 30);
            trpl::sleep(one_ms).await;
            slow("a", 10);
            trpl::sleep(one_ms).await;
            slow("a", 20);
            trpl::sleep(one_ms).await;
            println!("'a' finished.");
        };

        let b = async {
            println!("'b' started.");
            slow("b", 75);
            trpl::sleep(one_ms).await;
            slow("b", 10);
            trpl::sleep(one_ms).await;
            slow("b", 15);
            trpl::sleep(one_ms).await;
            slow("b", 350);
            trpl::sleep(one_ms).await;
            println!("'b' finished.");
        };

        trpl::race(a, b).await;
    });
}

fn slow(name: &str, ms: u64) {
    thread::sleep(Duration::from_millis(ms));
    println!("'{name}' ran for {ms}ms");
}
extern crate trpl; // required for mdbook test

use std::{thread, time::Duration};

fn main() {
    trpl::run(async {
        let a = async {
            println!("'a' started.");
            slow("a", 30);
            trpl::yield_now().await;
            slow("a", 10);
            trpl::yield_now().await;
            slow("a", 20);
            trpl::yield_now().await;
            println!("'a' finished.");
        };

        let b = async {
            println!("'b' started.");
            slow("b", 75);
            trpl::yield_now().await;
            slow("b", 10);
            trpl::yield_now().await;
            slow("b", 15);
            trpl::yield_now().await;
            slow("b", 350);
            trpl::yield_now().await;
            println!("'b' finished.");
        };

        trpl::race(a, b).await;
    });
}

fn slow(name: &str, ms: u64) {
    thread::sleep(Duration::from_millis(ms));
    println!("'{name}' ran for {ms}ms");
}

Performance comparison: yield_now() is significantly faster than sleep() for cooperation points.

extern crate trpl; // required for mdbook test

use std::time::{Duration, Instant};

fn main() {
    trpl::run(async {
        let one_ns = Duration::from_nanos(1);
        let start = Instant::now();
        async {
            for _ in 1..1000 {
                trpl::sleep(one_ns).await;
            }
        }
        .await;
        let time = Instant::now() - start;
        println!(
            "'sleep' version finished after {} seconds.",
            time.as_secs_f32()
        );

        let start = Instant::now();
        async {
            for _ in 1..1000 {
                trpl::yield_now().await;
            }
        }
        .await;
        let time = Instant::now() - start;
        println!(
            "'yield' version finished after {} seconds.",
            time.as_secs_f32()
        );
    });
}

Building Async Abstractions

Compose futures to create higher-level abstractions:

extern crate trpl; // required for mdbook test

use std::time::Duration;

fn main() {
    trpl::run(async {
        let slow = async {
            trpl::sleep(Duration::from_millis(100)).await;
            "I finished!"
        };

        match timeout(slow, Duration::from_millis(10)).await {
            Ok(message) => println!("Succeeded with '{message}'"),
            Err(duration) => {
                println!("Failed after {} seconds", duration.as_secs())
            }
        }
    });
}
extern crate trpl; // required for mdbook test

use std::time::Duration;

fn main() {
    trpl::run(async {
        let slow = async {
            trpl::sleep(Duration::from_secs(5)).await;
            "Finally finished"
        };

        match timeout(slow, Duration::from_millis(10)).await {
            Ok(message) => println!("Succeeded with '{message}'"),
            Err(duration) => {
                println!("Failed after {} seconds", duration.as_secs())
            }
        }
    });
}

async fn timeout<F: Future>(
    future_to_try: F,
    max_time: Duration,
) -> Result<F::Output, Duration> {
    // Here is where our implementation will go!
}
extern crate trpl; // required for mdbook test

use std::time::Duration;

use trpl::Either;

// --snip--

fn main() {
    trpl::run(async {
        let slow = async {
            trpl::sleep(Duration::from_secs(5)).await;
            "Finally finished"
        };

        match timeout(slow, Duration::from_secs(2)).await {
            Ok(message) => println!("Succeeded with '{message}'"),
            Err(duration) => {
                println!("Failed after {} seconds", duration.as_secs())
            }
        }
    });
}

async fn timeout<F: Future>(
    future_to_try: F,
    max_time: Duration,
) -> Result<F::Output, Duration> {
    match trpl::race(future_to_try, trpl::sleep(max_time)).await {
        Either::Left(output) => Ok(output),
        Either::Right(_) => Err(max_time),
    }
}

Pattern: Use race to implement timeout logic. First argument gets priority due to non-fair scheduling.

Key Patterns

  • Dynamic collections: Trait objects + pinning for runtime-determined future sets
  • Static collections: join! macro for compile-time known futures
  • Cooperation: Insert yield_now() in CPU-intensive loops
  • Composition: Build complex async behavior from simpler primitives
  • Performance: Choose between stack (pin!) and heap (Box::pin) allocation

Streams: Async Iterators

Streams provide async iteration over sequences of values. They’re the async equivalent of Iterator, yielding items over time rather than immediately.

Core Concepts

Stream: Async equivalent of Iterator StreamExt: Extension trait providing utility methods (like IteratorExt)

Basic stream from iterator:

extern crate trpl; // required for mdbook test

fn main() {
    trpl::run(async {
        let values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
        let iter = values.iter().map(|n| n * 2);
        let mut stream = trpl::stream_from_iter(iter);

        while let Some(value) = stream.next().await {
            println!("The value was: {value}");
        }
    });
}

Error: Missing StreamExt trait for the next() method.

extern crate trpl; // required for mdbook test

use trpl::StreamExt;

fn main() {
    trpl::run(async {
        let values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
        let iter = values.iter().map(|n| n * 2);
        let mut stream = trpl::stream_from_iter(iter);

        while let Some(value) = stream.next().await {
            println!("The value was: {value}");
        }
    });
}

Stream Processing

Streams support familiar iterator patterns:

extern crate trpl; // required for mdbook test

use trpl::StreamExt;

fn main() {
    trpl::run(async {
        let values = 1..101;
        let iter = values.map(|n| n * 2);
        let stream = trpl::stream_from_iter(iter);

        let mut filtered =
            stream.filter(|value| value % 3 == 0 || value % 5 == 0);

        while let Some(value) = filtered.next().await {
            println!("The value was: {value}");
        }
    });
}

Real-time Data Streams

Streams excel at processing real-time data like WebSocket messages or event streams:

extern crate trpl; // required for mdbook test

use trpl::{ReceiverStream, Stream, StreamExt};

fn main() {
    trpl::run(async {
        let mut messages = get_messages();

        while let Some(message) = messages.next().await {
            println!("{message}");
        }
    });
}

fn get_messages() -> impl Stream<Item = String> {
    let (tx, rx) = trpl::channel();

    let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"];
    for message in messages {
        tx.send(format!("Message: '{message}'")).unwrap();
    }

    ReceiverStream::new(rx)
}

ReceiverStream converts async channel receivers into streams.

Timeouts on Streams

Apply timeouts to individual stream items:

extern crate trpl; // required for mdbook test

use std::{pin::pin, time::Duration};
use trpl::{ReceiverStream, Stream, StreamExt};

fn main() {
    trpl::run(async {
        let mut messages =
            pin!(get_messages().timeout(Duration::from_millis(200)));

        while let Some(result) = messages.next().await {
            match result {
                Ok(message) => println!("{message}"),
                Err(reason) => eprintln!("Problem: {reason:?}"),
            }
        }
    })
}

fn get_messages() -> impl Stream<Item = String> {
    let (tx, rx) = trpl::channel();

    let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"];
    for message in messages {
        tx.send(format!("Message: '{message}'")).unwrap();
    }

    ReceiverStream::new(rx)
}

Add delays to test timeout behavior:

extern crate trpl; // required for mdbook test

use std::{pin::pin, time::Duration};

use trpl::{ReceiverStream, Stream, StreamExt};

fn main() {
    trpl::run(async {
        let mut messages =
            pin!(get_messages().timeout(Duration::from_millis(200)));

        while let Some(result) = messages.next().await {
            match result {
                Ok(message) => println!("{message}"),
                Err(reason) => eprintln!("Problem: {reason:?}"),
            }
        }
    })
}

fn get_messages() -> impl Stream<Item = String> {
    let (tx, rx) = trpl::channel();

    trpl::spawn_task(async move {
        let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"];
        for (index, message) in messages.into_iter().enumerate() {
            let time_to_sleep = if index % 2 == 0 { 100 } else { 300 };
            trpl::sleep(Duration::from_millis(time_to_sleep)).await;

            tx.send(format!("Message: '{message}'")).unwrap();
        }
    });

    ReceiverStream::new(rx)
}

Key point: Use spawn_task to avoid blocking the stream creation. The async delays happen concurrently with stream consumption.

Interval Streams

Create periodic streams for regular events:

extern crate trpl; // required for mdbook test

use std::{pin::pin, time::Duration};

use trpl::{ReceiverStream, Stream, StreamExt};

fn main() {
    trpl::run(async {
        let mut messages =
            pin!(get_messages().timeout(Duration::from_millis(200)));

        while let Some(result) = messages.next().await {
            match result {
                Ok(message) => println!("{message}"),
                Err(reason) => eprintln!("Problem: {reason:?}"),
            }
        }
    })
}

fn get_messages() -> impl Stream<Item = String> {
    let (tx, rx) = trpl::channel();

    trpl::spawn_task(async move {
        let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"];
        for (index, message) in messages.into_iter().enumerate() {
            let time_to_sleep = if index % 2 == 0 { 100 } else { 300 };
            trpl::sleep(Duration::from_millis(time_to_sleep)).await;

            tx.send(format!("Message: '{message}'")).unwrap();
        }
    });

    ReceiverStream::new(rx)
}

fn get_intervals() -> impl Stream<Item = u32> {
    let (tx, rx) = trpl::channel();

    trpl::spawn_task(async move {
        let mut count = 0;
        loop {
            trpl::sleep(Duration::from_millis(1)).await;
            count += 1;
            tx.send(count).unwrap();
        }
    });

    ReceiverStream::new(rx)
}

Merging Streams

Combine multiple streams into one:

extern crate trpl; // required for mdbook test

use std::{pin::pin, time::Duration};

use trpl::{ReceiverStream, Stream, StreamExt};

fn main() {
    trpl::run(async {
        let messages = get_messages().timeout(Duration::from_millis(200));
        let intervals = get_intervals();
        let merged = messages.merge(intervals);

        while let Some(result) = merged.next().await {
            match result {
                Ok(message) => println!("{message}"),
                Err(reason) => eprintln!("Problem: {reason:?}"),
            }
        }
    })
}

fn get_messages() -> impl Stream<Item = String> {
    let (tx, rx) = trpl::channel();

    trpl::spawn_task(async move {
        let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"];
        for (index, message) in messages.into_iter().enumerate() {
            let time_to_sleep = if index % 2 == 0 { 100 } else { 300 };
            trpl::sleep(Duration::from_millis(time_to_sleep)).await;

            tx.send(format!("Message: '{message}'")).unwrap();
        }
    });

    ReceiverStream::new(rx)
}

fn get_intervals() -> impl Stream<Item = u32> {
    let (tx, rx) = trpl::channel();

    trpl::spawn_task(async move {
        let mut count = 0;
        loop {
            trpl::sleep(Duration::from_millis(1)).await;
            count += 1;
            tx.send(count).unwrap();
        }
    });

    ReceiverStream::new(rx)
}

Error: Type mismatch between streams. Need to align types:

extern crate trpl; // required for mdbook test

use std::{pin::pin, time::Duration};

use trpl::{ReceiverStream, Stream, StreamExt};

fn main() {
    trpl::run(async {
        let messages = get_messages().timeout(Duration::from_millis(200));
        let intervals = get_intervals()
            .map(|count| format!("Interval: {count}"))
            .timeout(Duration::from_secs(10));
        let merged = messages.merge(intervals);
        let mut stream = pin!(merged);

        while let Some(result) = stream.next().await {
            match result {
                Ok(message) => println!("{message}"),
                Err(reason) => eprintln!("Problem: {reason:?}"),
            }
        }
    })
}

fn get_messages() -> impl Stream<Item = String> {
    let (tx, rx) = trpl::channel();

    trpl::spawn_task(async move {
        let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"];
        for (index, message) in messages.into_iter().enumerate() {
            let time_to_sleep = if index % 2 == 0 { 100 } else { 300 };
            trpl::sleep(Duration::from_millis(time_to_sleep)).await;

            tx.send(format!("Message: '{message}'")).unwrap();
        }
    });

    ReceiverStream::new(rx)
}

fn get_intervals() -> impl Stream<Item = u32> {
    let (tx, rx) = trpl::channel();

    trpl::spawn_task(async move {
        let mut count = 0;
        loop {
            trpl::sleep(Duration::from_millis(1)).await;
            count += 1;
            tx.send(count).unwrap();
        }
    });

    ReceiverStream::new(rx)
}

Stream Rate Control

Control stream throughput with throttle and limit with take:

extern crate trpl; // required for mdbook test

use std::{pin::pin, time::Duration};

use trpl::{ReceiverStream, Stream, StreamExt};

fn main() {
    trpl::run(async {
        let messages = get_messages().timeout(Duration::from_millis(200));
        let intervals = get_intervals()
            .map(|count| format!("Interval: {count}"))
            .throttle(Duration::from_millis(100))
            .timeout(Duration::from_secs(10));
        let merged = messages.merge(intervals).take(20);
        let mut stream = pin!(merged);

        while let Some(result) = stream.next().await {
            match result {
                Ok(message) => println!("{message}"),
                Err(reason) => eprintln!("Problem: {reason:?}"),
            }
        }
    })
}

fn get_messages() -> impl Stream<Item = String> {
    let (tx, rx) = trpl::channel();

    trpl::spawn_task(async move {
        let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"];
        for (index, message) in messages.into_iter().enumerate() {
            let time_to_sleep = if index % 2 == 0 { 100 } else { 300 };
            trpl::sleep(Duration::from_millis(time_to_sleep)).await;

            tx.send(format!("Message: '{message}'")).unwrap();
        }
    });

    ReceiverStream::new(rx)
}

fn get_intervals() -> impl Stream<Item = u32> {
    let (tx, rx) = trpl::channel();

    trpl::spawn_task(async move {
        let mut count = 0;
        loop {
            trpl::sleep(Duration::from_millis(1)).await;
            count += 1;
            tx.send(count).unwrap();
        }
    });

    ReceiverStream::new(rx)
}

Throttle: Limits polling rate, not just output filtering Take: Limits total number of items processed Lazy evaluation: Unprocessed items are never generated (performance benefit)

Error Handling

Handle stream errors gracefully:

extern crate trpl; // required for mdbook test

use std::{pin::pin, time::Duration};

use trpl::{ReceiverStream, Stream, StreamExt};

fn main() {
    trpl::run(async {
        let messages = get_messages().timeout(Duration::from_millis(200));
        let intervals = get_intervals()
            .map(|count| format!("Interval #{count}"))
            .throttle(Duration::from_millis(500))
            .timeout(Duration::from_secs(10));
        let merged = messages.merge(intervals).take(20);
        let mut stream = pin!(merged);

        while let Some(result) = stream.next().await {
            match result {
                Ok(item) => println!("{item}"),
                Err(reason) => eprintln!("Problem: {reason:?}"),
            }
        }
    });
}

fn get_messages() -> impl Stream<Item = String> {
    let (tx, rx) = trpl::channel();

    trpl::spawn_task(async move {
        let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"];

        for (index, message) in messages.into_iter().enumerate() {
            let time_to_sleep = if index % 2 == 0 { 100 } else { 300 };
            trpl::sleep(Duration::from_millis(time_to_sleep)).await;

            if let Err(send_error) = tx.send(format!("Message: '{message}'")) {
                eprintln!("Cannot send message '{message}': {send_error}");
                break;
            }
        }
    });

    ReceiverStream::new(rx)
}

fn get_intervals() -> impl Stream<Item = u32> {
    let (tx, rx) = trpl::channel();

    trpl::spawn_task(async move {
        let mut count = 0;
        loop {
            trpl::sleep(Duration::from_millis(1)).await;
            count += 1;

            if let Err(send_error) = tx.send(count) {
                eprintln!("Could not send interval {count}: {send_error}");
                break;
            };
        }
    });

    ReceiverStream::new(rx)
}

Stream Patterns

Data Processing: Transform async data with map, filter, fold Rate Limiting: Use throttle to control processing rate Merging: Combine multiple event sources with merge Timeouts: Apply deadlines to individual stream items Backpressure: Control memory usage with take and buffering

Key differences from synchronous iterators:

  • Items arrive over time (not immediately available)
  • Support timeouts and cancellation
  • Enable concurrent processing of multiple streams
  • Natural fit for event-driven architectures

Performance consideration: Streams are lazy - items are only produced when polled. This enables efficient resource usage and backpressure handling.

Async Traits Deep Dive

Understanding the underlying traits enables effective async programming and troubleshooting.

The Future Trait

use std::pin::Pin;
use std::task::{Context, Poll};

pub trait Future {
    type Output;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

Key components:

  • Output: The value produced when the future completes
  • poll(): State machine driver, called by the runtime
  • Poll<T>: Either Ready(T) or Pending
enum Poll<T> {
    Ready(T),
    Pending,
}

Runtime interaction: await compiles to repeated poll() calls in a loop:

let mut page_title_fut = page_title(url);
loop {
    match page_title_fut.poll(cx) {
        Ready(value) => return value,
        Pending => {
            // Runtime schedules other work, resumes later
        }
    }
}

The Context parameter enables runtime coordination - futures register wake-up conditions.

Pin and Unpin

Problem: Async state machines can contain self-references. Moving such structures breaks internal pointers.

// Simplified state machine representation
struct AsyncStateMachine {
    state: State,
    data: String,
    reference_to_data: Option<&String>, // Self-reference!
}

Solution: Pin<T> prevents moving the pointed-to value.

// Pin prevents the inner value from moving
let pinned: Pin<Box<AsyncStateMachine>> = Box::pin(state_machine);

Unpin marker trait: Types safe to move even when pinned. Most types implement Unpin automatically.

// Most types can be moved freely
let string = String::from("hello");
let pinned_string = Pin::new(&mut string); // OK - String: Unpin

// Self-referential futures need pinning
let future = async { /* complex state machine */ };
let pinned_future = Box::pin(future); // Required for join_all()

Practical implications:

  • Most code doesn’t deal with Pin directly
  • join_all() requires Unpin because it stores futures in collections
  • Use Box::pin() or pin!() macro when required
  • Stack allocation (pin!) vs heap allocation (Box::pin)
use std::pin::pin;

// Stack pinning (preferred when lifetime allows)
let fut = pin!(async_operation());

// Heap pinning (needed for collections or longer lifetimes)
let fut = Box::pin(async_operation());

The Stream Trait

Streams combine Iterator and Future concepts:

use std::pin::Pin;
use std::task::{Context, Poll};

trait Stream {
    type Item;

    fn poll_next(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>
    ) -> Poll<Option<Self::Item>>;
}

Comparison:

  • Iterator::next()Option<Item> (synchronous)
  • Future::poll()Poll<Output> (async ready/pending)
  • Stream::poll_next()Poll<Option<Item>> (async sequence)

StreamExt trait: Provides high-level methods like next(), map(), filter():

// Simplified StreamExt implementation
trait StreamExt: Stream {
    async fn next(&mut self) -> Option<Self::Item> 
    where 
        Self: Unpin
    {
        // Implementation uses poll_next() internally
    }

    fn map<F, U>(self, f: F) -> Map<Self, F> 
    where 
        F: FnMut(Self::Item) -> U
    {
        // Returns a new stream that applies f to each item
    }
}

Practical Guidelines

Future trait: Rarely implemented directly. Use async fn and async {} blocks.

Pin/Unpin:

  • Most types are Unpin (can be moved freely)
  • Async blocks often require pinning for collections
  • Use pin!() for stack allocation, Box::pin() for heap

Stream trait:

  • Use StreamExt methods for stream operations
  • Similar to iterator patterns but async
  • Enables backpressure and cancellation

Error patterns:

// Error: Missing StreamExt
let mut stream = some_stream();
let item = stream.next().await; // ❌ No method `next`

// Fix: Import StreamExt
use futures::StreamExt;
let item = stream.next().await; // ✅ Works

// Error: Unpin requirement
let futures = vec![Box::new(async_block())]; // ❌ Unpin not satisfied

// Fix: Pin the futures
let futures = vec![Box::pin(async_block())]; // ✅ Works

When to use each:

  • Future: For single async operations
  • Stream: For async sequences over time
  • Pin: When collections or trait objects are involved
  • StreamExt: For stream transformations and operations

The traits form a coherent system where:

  1. Future provides the foundation for async operations
  2. Pin enables safe self-referential state machines
  3. Stream extends futures to sequences
  4. Extension traits provide ergonomic APIs

Futures, Tasks, and Threads

Understanding when to use threads vs async requires considering their different tradeoffs and capabilities.

Execution Models

Threads:

  • OS-managed, preemptive multitasking
  • Heavy resource overhead (~2-8MB stack per thread)
  • True parallelism on multi-core systems
  • Context switches managed by OS scheduler

Tasks (Async):

  • Runtime-managed, cooperative multitasking
  • Lightweight (~KB overhead per task)
  • Concurrency via yielding at await points
  • Can run millions concurrently

Interchangeable APIs

Many patterns work with both models:

extern crate trpl; // required for mdbook test

use std::{pin::pin, thread, time::Duration};

use trpl::{ReceiverStream, Stream, StreamExt};

fn main() {
    trpl::run(async {
        let messages = get_messages().timeout(Duration::from_millis(200));
        let intervals = get_intervals()
            .map(|count| format!("Interval #{count}"))
            .throttle(Duration::from_millis(500))
            .timeout(Duration::from_secs(10));
        let merged = messages.merge(intervals).take(20);
        let mut stream = pin!(merged);

        while let Some(result) = stream.next().await {
            match result {
                Ok(item) => println!("{item}"),
                Err(reason) => eprintln!("Problem: {reason:?}"),
            }
        }
    });
}

fn get_messages() -> impl Stream<Item = String> {
    let (tx, rx) = trpl::channel();

    trpl::spawn_task(async move {
        let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"];

        for (index, message) in messages.into_iter().enumerate() {
            let time_to_sleep = if index % 2 == 0 { 100 } else { 300 };
            trpl::sleep(Duration::from_millis(time_to_sleep)).await;

            if let Err(send_error) = tx.send(format!("Message: '{message}'")) {
                eprintln!("Cannot send message '{message}': {send_error}");
                break;
            }
        }
    });

    ReceiverStream::new(rx)
}

fn get_intervals() -> impl Stream<Item = u32> {
    let (tx, rx) = trpl::channel();

    // This is *not* `trpl::spawn` but `std::thread::spawn`!
    thread::spawn(move || {
        let mut count = 0;
        loop {
            // Likewise, this is *not* `trpl::sleep` but `std::thread::sleep`!
            thread::sleep(Duration::from_millis(1));
            count += 1;

            if let Err(send_error) = tx.send(count) {
                eprintln!("Could not send interval {count}: {send_error}");
                break;
            };
        }
    });

    ReceiverStream::new(rx)
}

Key differences:

  • thread::spawn vs trpl::spawn_task
  • thread::sleep vs trpl::sleep
  • Both return handles that can be awaited/joined
  • Same resulting streams despite different execution models

Architectural Hierarchy

Runtime (manages tasks)
├── Task 1 (manages futures)
│   ├── Future A
│   ├── Future B
│   └── Future C
├── Task 2 (manages futures)
│   └── Future D
└── OS Threads (work-stealing execution)
    ├── Thread 1 
    ├── Thread 2
    └── Thread 3

Three levels of concurrency:

  1. Futures: Finest granularity, cooperative yield points
  2. Tasks: Group related futures, runtime-scheduled
  3. Threads: OS-scheduled, can run tasks in parallel

Performance Characteristics

Threads excel at:

  • CPU-intensive work (parallel computation)
  • Blocking operations without async alternatives
  • Fire-and-forget background work
  • Simple parallelism requirements

Async excels at:

  • IO-intensive operations (network, file system)
  • High-concurrency scenarios (thousands of connections)
  • Structured concurrency patterns
  • Resource-constrained environments

Hybrid Patterns

Combine both approaches for optimal performance:

extern crate trpl; // for mdbook test

use std::{thread, time::Duration};

fn main() {
    let (tx, mut rx) = trpl::channel();

    thread::spawn(move || {
        for i in 1..11 {
            tx.send(i).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });

    trpl::run(async {
        while let Some(message) = rx.recv().await {
            println!("{message}");
        }
    });
}

Pattern: Use threads for blocking operations, async channels for coordination.

Work-Stealing Runtimes

Modern async runtimes use thread pools with work-stealing:

┌─────────────────────────────────────────┐
│ Async Runtime                           │
├─────────────────────────────────────────┤
│ Task Queue: [Task1, Task2, Task3, ...]  │
├─────────────────────────────────────────┤
│ Thread Pool:                            │
│ ├─ Worker Thread 1 ──┐                  │
│ ├─ Worker Thread 2 ──┼─ Work Stealing   │  
│ ├─ Worker Thread 3 ──┘                  │
│ └─ Worker Thread 4                      │
└─────────────────────────────────────────┘

Tasks can migrate between threads for load balancing, combining the benefits of both models.

Decision Framework

Choose Threads when:

  • Heavy CPU computation (image processing, mathematical calculations)
  • Blocking C libraries without async bindings
  • Simple parallel algorithms (embarrassingly parallel problems)
  • Need guaranteed execution progress (real-time systems)

Choose Async when:

  • Network servers handling many connections
  • IO-heavy applications (file processing, database queries)
  • Event-driven architectures
  • Memory-constrained environments

Use Both when:

  • Web applications (async for request handling, threads for background jobs)
  • Data pipelines (async for coordination, threads for CPU processing)
  • Gaming (async for network, threads for physics/rendering)

Cancellation and Cleanup

Threads: Limited cancellation support, manual cleanup

// Thread cancellation is tricky
let handle = thread::spawn(|| {
    // No built-in cancellation mechanism
    loop { /* work */ }
});
// Must use external signaling

Async: Built-in cancellation when futures are dropped

// Automatic cleanup when dropped
let task = spawn_task(async {
    some_operation().await; // Cancelled if task is dropped
});
drop(task); // Automatically cancels and cleans up

Performance Guidelines

Avoid:

  • Creating threads for each IO operation (thread-per-request anti-pattern)
  • Async for CPU-bound work without yield points
  • Mixing sync and async code unnecessarily

Optimize:

  • Use spawn_blocking for sync code in async contexts
  • Pool threads for repeated CPU work
  • Batch async operations to reduce overhead
  • Choose appropriate buffer sizes for channels

Summary

Threads: OS-managed parallelism, higher overhead, preemptive scheduling Tasks: Runtime-managed concurrency, lightweight, cooperative scheduling
Futures: Building blocks for async state machines

The choice isn’t exclusive - modern applications often use both approaches where each excels. Async provides better resource utilization for IO-bound work, while threads provide true parallelism for CPU-bound tasks.

Best practice: Start with async for concurrency needs, add threads for parallelism where beneficial. Use work-stealing runtimes that provide both models efficiently.

Object-Oriented Programming Features

Rust supports some object-oriented patterns but differs significantly from traditional OOP languages. This chapter examines how Rust implements OOP concepts like encapsulation and polymorphism through its type system, and demonstrates implementing the state pattern both traditionally and idiomatically in Rust.

Key topics:

  • Objects, encapsulation, and inheritance in Rust
  • Trait objects for runtime polymorphism
  • State pattern implementation and trade-offs
  • Type-based alternatives to traditional OOP patterns

Characteristics of Object-Oriented Languages

Rust implements OOP concepts differently than traditional languages. Here’s how Rust handles the core OOP characteristics:

Objects: Data + Behavior

Rust structs and enums with impl blocks provide the same functionality as objects - they package data and methods together:

pub struct AveragedCollection {
    list: Vec<i32>,
    average: f64,
}

Encapsulation

Rust uses pub keyword for visibility control. By default, everything is private:

pub struct AveragedCollection {
    list: Vec<i32>,
    average: f64,
}

Public methods control access to private data:

pub struct AveragedCollection {
    list: Vec<i32>,
    average: f64,
}

impl AveragedCollection {
    pub fn add(&mut self, value: i32) {
        self.list.push(value);
        self.update_average();
    }

    pub fn remove(&mut self) -> Option<i32> {
        let result = self.list.pop();
        match result {
            Some(value) => {
                self.update_average();
                Some(value)
            }
            None => None,
        }
    }

    pub fn average(&self) -> f64 {
        self.average
    }

    fn update_average(&mut self) {
        let total: i32 = self.list.iter().sum();
        self.average = total as f64 / self.list.len() as f64;
    }
}

Private fields prevent direct manipulation, ensuring data consistency. The internal implementation can change (e.g., from Vec<i32> to HashSet<i32>) without breaking external code.

Inheritance: Not Supported

Rust has no traditional inheritance. Instead:

For code reuse: Use default trait implementations:

trait Summary {
    fn summarize(&self) -> String {
        "Default implementation".to_string()
    }
}

For polymorphism: Use trait objects or generics with trait bounds:

// Trait objects (dynamic dispatch)
fn process(item: &dyn Summary) { }

// Generics (static dispatch) 
fn process<T: Summary>(item: &T) { }

Rust’s approach avoids inheritance issues like tight coupling and fragile base class problems. Trait objects enable polymorphism where different types can be treated uniformly if they implement the same trait.

Using Trait Objects for Runtime Polymorphism

Trait objects enable storing different types in the same collection when they implement a common trait. They provide dynamic dispatch at runtime cost.

Basic Usage

Define a trait for common behavior:

pub trait Draw {
    fn draw(&self);
}

Store trait objects using Box<dyn Trait>:

pub trait Draw {
    fn draw(&self);
}

pub struct Screen {
    pub components: Vec<Box<dyn Draw>>,
}

Call methods on trait objects:

pub trait Draw {
    fn draw(&self);
}

pub struct Screen {
    pub components: Vec<Box<dyn Draw>>,
}

impl Screen {
    pub fn run(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}

Trait Objects vs. Generics

Generics (monomorphization - static dispatch):

pub trait Draw {
    fn draw(&self);
}

pub struct Screen<T: Draw> {
    pub components: Vec<T>,
}

impl<T> Screen<T>
where
    T: Draw,
{
    pub fn run(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}
  • All components must be the same type
  • Zero-cost abstraction (compile-time dispatch)
  • Code duplication for each concrete type

Trait Objects (dynamic dispatch):

  • Mixed types in same collection
  • Runtime cost for method lookup
  • Single implementation in binary

Implementation Examples

pub trait Draw {
    fn draw(&self);
}

pub struct Screen {
    pub components: Vec<Box<dyn Draw>>,
}

impl Screen {
    pub fn run(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}

pub struct Button {
    pub width: u32,
    pub height: u32,
    pub label: String,
}

impl Draw for Button {
    fn draw(&self) {
        // code to actually draw a button
    }
}
use gui::Draw;

struct SelectBox {
    width: u32,
    height: u32,
    options: Vec<String>,
}

impl Draw for SelectBox {
    fn draw(&self) {
        // code to actually draw a select box
    }
}

fn main() {}
use gui::Draw;

struct SelectBox {
    width: u32,
    height: u32,
    options: Vec<String>,
}

impl Draw for SelectBox {
    fn draw(&self) {
        // code to actually draw a select box
    }
}

use gui::{Button, Screen};

fn main() {
    let screen = Screen {
        components: vec![
            Box::new(SelectBox {
                width: 75,
                height: 10,
                options: vec![
                    String::from("Yes"),
                    String::from("Maybe"),
                    String::from("No"),
                ],
            }),
            Box::new(Button {
                width: 50,
                height: 10,
                label: String::from("OK"),
            }),
        ],
    };

    screen.run();
}

Compile-time Safety

Rust enforces trait implementation at compile time:

use gui::Screen;

fn main() {
    let screen = Screen {
        components: vec![Box::new(String::from("Hi"))],
    };

    screen.run();
}
$ cargo run
   Compiling gui v0.1.0 (file:///projects/gui)
error[E0277]: the trait bound `String: Draw` is not satisfied
 --> src/main.rs:5:26
  |
5 |         components: vec![Box::new(String::from("Hi"))],
  |                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Draw` is not implemented for `String`
  |
  = help: the trait `Draw` is implemented for `Button`
  = note: required for the cast from `Box<String>` to `Box<dyn Draw>`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `gui` (bin "gui") due to 1 previous error

Performance Trade-offs

Static Dispatch (generics):

  • Faster execution (no runtime lookup)
  • Larger binary size (code duplication)
  • All types known at compile time

Dynamic Dispatch (trait objects):

  • Runtime method lookup overhead
  • Smaller binary size
  • Enables heterogeneous collections
  • Prevents some compiler optimizations

Choose based on your use case: use generics for performance-critical code with known types, trait objects for flexible APIs with mixed types.

Implementing Design Patterns in Rust

The state pattern encodes states as separate objects, with behavior changing based on internal state. This demonstrates two approaches: traditional OOP-style implementation and idiomatic Rust using the type system.

Traditional State Pattern

The blog post workflow requires:

  1. Draft → Review → Published transitions
  2. Only published posts display content
  3. Invalid transitions are ignored
use blog::Post;

fn main() {
    let mut post = Post::new();

    post.add_text("I ate a salad for lunch today");
    assert_eq!("", post.content());

    post.request_review();
    assert_eq!("", post.content());

    post.approve();
    assert_eq!("I ate a salad for lunch today", post.content());
}

Implementation

pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }
}

trait State {}

struct Draft {}

impl State for Draft {}
pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    // --snip--
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }

    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }
}

trait State {}

struct Draft {}

impl State for Draft {}
pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    // --snip--
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }

    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }

    pub fn content(&self) -> &str {
        ""
    }
}

trait State {}

struct Draft {}

impl State for Draft {}

State Transitions

pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    // --snip--
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }

    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }

    pub fn content(&self) -> &str {
        ""
    }

    pub fn request_review(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.request_review())
        }
    }
}

trait State {
    fn request_review(self: Box<Self>) -> Box<dyn State>;
}

struct Draft {}

impl State for Draft {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        Box::new(PendingReview {})
    }
}

struct PendingReview {}

impl State for PendingReview {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }
}
pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    // --snip--
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }

    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }

    pub fn content(&self) -> &str {
        ""
    }

    pub fn request_review(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.request_review())
        }
    }

    pub fn approve(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.approve())
        }
    }
}

trait State {
    fn request_review(self: Box<Self>) -> Box<dyn State>;
    fn approve(self: Box<Self>) -> Box<dyn State>;
}

struct Draft {}

impl State for Draft {
    // --snip--
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        Box::new(PendingReview {})
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

struct PendingReview {}

impl State for PendingReview {
    // --snip--
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        Box::new(Published {})
    }
}

struct Published {}

impl State for Published {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}
pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    // --snip--
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }

    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }

    pub fn content(&self) -> &str {
        self.state.as_ref().unwrap().content(self)
    }
    // --snip--

    pub fn request_review(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.request_review())
        }
    }

    pub fn approve(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.approve())
        }
    }
}

trait State {
    fn request_review(self: Box<Self>) -> Box<dyn State>;
    fn approve(self: Box<Self>) -> Box<dyn State>;
}

struct Draft {}

impl State for Draft {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        Box::new(PendingReview {})
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

struct PendingReview {}

impl State for PendingReview {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        Box::new(Published {})
    }
}

struct Published {}

impl State for Published {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}
pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }

    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }

    pub fn content(&self) -> &str {
        self.state.as_ref().unwrap().content(self)
    }

    pub fn request_review(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.request_review())
        }
    }

    pub fn approve(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.approve())
        }
    }
}

trait State {
    // --snip--
    fn request_review(self: Box<Self>) -> Box<dyn State>;
    fn approve(self: Box<Self>) -> Box<dyn State>;

    fn content<'a>(&self, post: &'a Post) -> &'a str {
        ""
    }
}

// --snip--

struct Draft {}

impl State for Draft {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        Box::new(PendingReview {})
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

struct PendingReview {}

impl State for PendingReview {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        Box::new(Published {})
    }
}

struct Published {}

impl State for Published {
    // --snip--
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn content<'a>(&self, post: &'a Post) -> &'a str {
        &post.content
    }
}

Trade-offs of Traditional State Pattern

Advantages:

  • State logic encapsulated in state objects
  • Adding new states requires minimal changes
  • Transitions managed internally

Disadvantages:

  • States coupled to each other
  • Logic duplication in transition methods
  • Runtime overhead from trait objects
  • Invalid states possible at runtime

Type-Based State Pattern

Rust’s type system can encode states as types, making invalid states impossible:

pub struct Post {
    content: String,
}

pub struct DraftPost {
    content: String,
}

impl Post {
    pub fn new() -> DraftPost {
        DraftPost {
            content: String::new(),
        }
    }

    pub fn content(&self) -> &str {
        &self.content
    }
}

impl DraftPost {
    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }
}
pub struct Post {
    content: String,
}

pub struct DraftPost {
    content: String,
}

impl Post {
    pub fn new() -> DraftPost {
        DraftPost {
            content: String::new(),
        }
    }

    pub fn content(&self) -> &str {
        &self.content
    }
}

impl DraftPost {
    // --snip--
    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }

    pub fn request_review(self) -> PendingReviewPost {
        PendingReviewPost {
            content: self.content,
        }
    }
}

pub struct PendingReviewPost {
    content: String,
}

impl PendingReviewPost {
    pub fn approve(self) -> Post {
        Post {
            content: self.content,
        }
    }
}
use blog::Post;

fn main() {
    let mut post = Post::new();

    post.add_text("I ate a salad for lunch today");

    let post = post.request_review();

    let post = post.approve();

    assert_eq!("I ate a salad for lunch today", post.content());
}

Type-Based Advantages

Compile-time Safety:

  • Invalid states impossible
  • Attempting to get content from draft fails to compile
  • State transitions enforced by type system

Performance:

  • No runtime state checks
  • No trait object overhead
  • Zero-cost abstractions

API Design:

  • Clear ownership semantics
  • Impossible to misuse API
  • Self-documenting through types

When to Use Each Pattern

Traditional State Pattern:

  • Need runtime flexibility
  • States determined by external data
  • Working with existing OOP codebases

Type-Based Pattern:

  • Compile-time state validation required
  • Performance critical code
  • Taking advantage of Rust’s type system

The type-based approach demonstrates Rust’s strength: encoding invariants in the type system prevents entire classes of bugs at compile time, with zero runtime cost.

Patterns and Matching

Patterns are syntactic constructs for matching against data structure shapes and extracting values. They work with match expressions, if let, while let, function parameters, and destructuring assignments.

Pattern components:

  • Literals: 42, "hello", true
  • Destructuring: (a, b), Some(x), Point { x, y }
  • Variables: x, mut counter
  • Wildcards: _, ..
  • Guards: x if x > 0

This chapter covers pattern usage contexts, refutability (whether patterns can fail to match), and comprehensive pattern syntax. Understanding patterns is essential for idiomatic Rust code involving data extraction and control flow.

Pattern Usage Contexts

match Arms

match VALUE {
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
}

match expressions must be exhaustive. Use _ as a catchall pattern:

match x {
    None => None,
    Some(i) => Some(i + 1),
}

Conditional if let Expressions

More flexible than match for single-pattern matching:

fn main() {
    let favorite_color: Option<&str> = None;
    let is_tuesday = false;
    let age: Result<u8, _> = "34".parse();

    if let Some(color) = favorite_color {
        println!("Using your favorite color, {color}, as the background");
    } else if is_tuesday {
        println!("Tuesday is green day!");
    } else if let Ok(age) = age {
        if age > 30 {
            println!("Using purple as the background color");
        } else {
            println!("Using orange as the background color");
        }
    } else {
        println!("Using blue as the background color");
    }
}

if let doesn’t check exhaustiveness, unlike match. Can introduce shadowed variables within the conditional scope.

while let Conditional Loops

fn main() {
    let (tx, rx) = std::sync::mpsc::channel();
    std::thread::spawn(move || {
        for val in [1, 2, 3] {
            tx.send(val).unwrap();
        }
    });

    while let Ok(value) = rx.recv() {
        println!("{value}");
    }
}

Continues until the pattern fails to match.

for Loops

The pattern follows for:

fn main() {
    let v = vec!['a', 'b', 'c'];

    for (index, value) in v.iter().enumerate() {
        println!("{value} is at index {index}");
    }
}

Output:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.52s
     Running `target/debug/patterns`
a is at index 0
b is at index 1
c is at index 2

let Statements

Every let statement uses a pattern:

let x = 5;  // x is a pattern

Formal syntax: let PATTERN = EXPRESSION;

Destructuring example:

fn main() {
    let (x, y, z) = (1, 2, 3);
}

Pattern elements must match exactly:

fn main() {
    let (x, y) = (1, 2, 3);
}

Error:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0308]: mismatched types
 --> src/main.rs:2:9
  |
2 |     let (x, y) = (1, 2, 3);
  |         ^^^^^^   --------- this expression has type `({integer}, {integer}, {integer})`
  |         |
  |         expected a tuple with 3 elements, found one with 2 elements
  |
  = note: expected tuple `({integer}, {integer}, {integer})`
             found tuple `(_, _)`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `patterns` (bin "patterns") due to 1 previous error

Function Parameters

Function parameters are patterns:

fn foo(x: i32) {
    // code goes here
}

fn main() {}

Destructuring in function signatures:

fn print_coordinates(&(x, y): &(i32, i32)) {
    println!("Current location: ({x}, {y})");
}

fn main() {
    let point = (3, 5);
    print_coordinates(&point);
}

Prints: Current location: (3, 5)

Closure parameters work the same way as function parameters.

Refutability: Whether a Pattern Might Fail to Match

Patterns are either irrefutable (always match) or refutable (can fail to match).

Irrefutable Patterns

  • x in let x = 5
  • (a, b) in let (a, b) = (1, 2)
  • Always succeed, can’t fail

Refutable Patterns

  • Some(x) in if let Some(x) = value
  • Can fail if value doesn’t match expected shape

Context Requirements

Irrefutable patterns only:

  • let statements
  • Function parameters
  • for loops

Refutable patterns accepted:

  • if let expressions
  • while let expressions
  • match arms (except final arm)

Common Errors

Using refutable pattern with let:

fn main() {
    let some_option_value: Option<i32> = None;
    let Some(x) = some_option_value;
}

Error:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0005]: refutable pattern in local binding
 --> src/main.rs:3:9
  |
3 |     let Some(x) = some_option_value;
  |         ^^^^^^^ pattern `None` not covered
  |
  = note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
  = note: for more information, visit https://doc.rust-lang.org/book/ch19-02-refutability.html
  = note: the matched value is of type `Option<i32>`
help: you might want to use `let else` to handle the variant that isn't matched
  |
3 |     let Some(x) = some_option_value else { todo!() };
  |                                     ++++++++++++++++

For more information about this error, try `rustc --explain E0005`.
error: could not compile `patterns` (bin "patterns") due to 1 previous error

Fix with let...else:

fn main() {
    let some_option_value: Option<i32> = None;
    let Some(x) = some_option_value else {
        return;
    };
}

Using irrefutable pattern with if let:

fn main() {
    let x = 5 else {
        return;
    };
}

Warning:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
warning: irrefutable `let...else` pattern
 --> src/main.rs:2:5
  |
2 |     let x = 5 else {
  |     ^^^^^^^^^
  |
  = note: this pattern will always match, so the `else` clause is useless
  = help: consider removing the `else` clause
  = note: `#[warn(irrefutable_let_patterns)]` on by default

warning: `patterns` (bin "patterns") generated 1 warning
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.39s
     Running `target/debug/patterns`

Summary: Use the right pattern type for the context. The compiler enforces these rules to prevent logic errors.

Pattern Syntax

Matching Literals

fn main() {
    let x = 1;

    match x {
        1 => println!("one"),
        2 => println!("two"),
        3 => println!("three"),
        _ => println!("anything"),
    }
}

Named Variables

Variables in patterns shadow outer scope variables:

fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        Some(y) => println!("Matched, y = {y}"),
        _ => println!("Default case, x = {x:?}"),
    }

    println!("at the end: x = {x:?}, y = {y}");
}

The inner y shadows the outer y within the match arm scope.

Multiple Patterns

Use | for multiple options:

fn main() {
    let x = 1;

    match x {
        1 | 2 => println!("one or two"),
        3 => println!("three"),
        _ => println!("anything"),
    }
}

Ranges with ..=

fn main() {
    let x = 5;

    match x {
        1..=5 => println!("one through five"),
        _ => println!("something else"),
    }
}

Character ranges:

fn main() {
    let x = 'c';

    match x {
        'a'..='j' => println!("early ASCII letter"),
        'k'..='z' => println!("late ASCII letter"),
        _ => println!("something else"),
    }
}

Destructuring

Structs

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    let Point { x: a, y: b } = p;
    assert_eq!(0, a);
    assert_eq!(7, b);
}

Shorthand syntax:

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    let Point { x, y } = p;
    assert_eq!(0, x);
    assert_eq!(7, y);
}

Combining with literals:

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    match p {
        Point { x, y: 0 } => println!("On the x axis at {x}"),
        Point { x: 0, y } => println!("On the y axis at {y}"),
        Point { x, y } => {
            println!("On neither axis: ({x}, {y})");
        }
    }
}

Enums

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {
    let msg = Message::ChangeColor(0, 160, 255);

    match msg {
        Message::Quit => {
            println!("The Quit variant has no data to destructure.");
        }
        Message::Move { x, y } => {
            println!("Move in the x direction {x} and in the y direction {y}");
        }
        Message::Write(text) => {
            println!("Text message: {text}");
        }
        Message::ChangeColor(r, g, b) => {
            println!("Change color to red {r}, green {g}, and blue {b}");
        }
    }
}

Nested Structures

enum Color {
    Rgb(i32, i32, i32),
    Hsv(i32, i32, i32),
}

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(Color),
}

fn main() {
    let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));

    match msg {
        Message::ChangeColor(Color::Rgb(r, g, b)) => {
            println!("Change color to red {r}, green {g}, and blue {b}");
        }
        Message::ChangeColor(Color::Hsv(h, s, v)) => {
            println!("Change color to hue {h}, saturation {s}, value {v}");
        }
        _ => (),
    }
}

Structs and Tuples Combined

fn main() {
    struct Point {
        x: i32,
        y: i32,
    }

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

Ignoring Values

Entire Value with _

fn foo(_: i32, y: i32) {
    println!("This code only uses the y parameter: {y}");
}

fn main() {
    foo(3, 4);
}

Parts of Values with Nested _

fn main() {
    let mut setting_value = Some(5);
    let new_setting_value = Some(10);

    match (setting_value, new_setting_value) {
        (Some(_), Some(_)) => {
            println!("Can't overwrite an existing customized value");
        }
        _ => {
            setting_value = new_setting_value;
        }
    }

    println!("setting is {setting_value:?}");
}
fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (first, _, third, _, fifth) => {
            println!("Some numbers: {first}, {third}, {fifth}");
        }
    }
}

Variables Starting with _

fn main() {
    let _x = 5;
    let y = 10;
}

Important difference: _x binds the value, _ doesn’t:

fn main() {
    let s = Some(String::from("Hello!"));

    if let Some(_s) = s {
        println!("found a string");
    }

    println!("{s:?}");
}
fn main() {
    let s = Some(String::from("Hello!"));

    if let Some(_) = s {
        println!("found a string");
    }

    println!("{s:?}");
}

Remaining Parts with ..

fn main() {
    struct Point {
        x: i32,
        y: i32,
        z: i32,
    }

    let origin = Point { x: 0, y: 0, z: 0 };

    match origin {
        Point { x, .. } => println!("x is {x}"),
    }
}
fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (first, .., last) => {
            println!("Some numbers: {first}, {last}");
        }
    }
}

Ambiguous usage fails:

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (.., second, ..) => {
            println!("Some numbers: {second}")
        },
    }
}

Error:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
error: `..` can only be used once per tuple pattern
 --> src/main.rs:5:22
  |
5 |         (.., second, ..) => {
  |          --          ^^ can only be used once per tuple pattern
  |          |
  |          previously used here

error: could not compile `patterns` (bin "patterns") due to 1 previous error

Match Guards

Additional if conditions in match arms:

fn main() {
    let num = Some(4);

    match num {
        Some(x) if x % 2 == 0 => println!("The number {x} is even"),
        Some(x) => println!("The number {x} is odd"),
        None => (),
    }
}

Match guards solve variable shadowing:

fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        Some(n) if n == y => println!("Matched, n = {n}"),
        _ => println!("Default case, x = {x:?}"),
    }

    println!("at the end: x = {x:?}, y = {y}");
}

Multiple patterns with guards:

fn main() {
    let x = 4;
    let y = false;

    match x {
        4 | 5 | 6 if y => println!("yes"),
        _ => println!("no"),
    }
}

The guard applies to all patterns: (4 | 5 | 6) if y, not 4 | 5 | (6 if y).

@ Bindings

Bind a value while testing it:

fn main() {
    enum Message {
        Hello { id: i32 },
    }

    let msg = Message::Hello { id: 5 };

    match msg {
        Message::Hello {
            id: id_variable @ 3..=7,
        } => println!("Found an id in range: {id_variable}"),
        Message::Hello { id: 10..=12 } => {
            println!("Found an id in another range")
        }
        Message::Hello { id } => println!("Found some other id: {id}"),
    }
}

@ lets you capture a value that matches a range or other complex pattern for use in the associated code block.

Summary

Patterns provide powerful data extraction and control flow capabilities. Key features:

  • Destructuring: Extract values from complex data structures
  • Refutability: Compiler ensures pattern safety in different contexts
  • Guards: Add conditional logic to patterns
  • Binding: Capture values with @ while pattern matching
  • Ignoring: Use _ and .. to ignore unneeded values

Mastering patterns is essential for idiomatic Rust code involving enums, structs, and control flow.

Advanced Features

Advanced Rust features for low-level control and systems programming scenarios:

  • Unsafe Rust: Raw pointers, FFI, static variables
  • Advanced traits: Associated types, disambiguation, supertraits
  • Advanced types: Type aliases, never type, DSTs
  • Functions/closures: Function pointers, returning closures
  • Macros: Compile-time code generation

Use when standard abstractions are insufficient for performance or interop requirements.

Unsafe Rust

Bypass Rust’s compile-time safety checks for low-level operations, FFI, and performance-critical code.

Unsafe Operations

Five operations require unsafe blocks:

unsafe {
    // Dereference raw pointers
    // Call unsafe functions
    // Access/modify mutable statics  
    // Implement unsafe traits
    // Access union fields
}

Raw Pointers

let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;

unsafe {
    println!("r1: {}", *r1);  // Dereferencing requires unsafe
}

Key differences from references:

  • No borrow checking (multiple mutable pointers allowed)
  • Can be null or point to invalid memory
  • No automatic cleanup

Safe Abstractions

Wrap unsafe code in safe APIs:

fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    assert!(mid <= values.len());
    unsafe {
        let ptr = values.as_mut_ptr();
        (
            slice::from_raw_parts_mut(ptr, mid),
            slice::from_raw_parts_mut(ptr.add(mid), values.len() - mid),
        )
    }
}

FFI (Foreign Function Interface)

extern "C" {
    fn abs(input: i32) -> i32;
}

unsafe {
    println!("Absolute value: {}", abs(-3));
}

Exporting to C:

#[no_mangle]
pub extern "C" fn call_from_c() {
    println!("Called from C!");
}

Static Variables

static mut COUNTER: usize = 0;

unsafe {
    COUNTER += 1;
    println!("COUNTER: {}", COUNTER);
}

Unsafe Traits

unsafe trait Foo {
    // ...
}

unsafe impl Foo for i32 {
    // ...
}

Unions

union MyUnion {
    f1: u32,
    f2: f32,
}

let u = MyUnion { f1: 1 };
unsafe {
    match u {
        MyUnion { f1: i } => println!("i32: {}", i),
    }
}

Miri Validation

Detect undefined behavior in unsafe code:

cargo +nightly miri run
cargo +nightly miri test

Best Practices

  1. Minimize scope: Keep unsafe blocks small and focused
  2. Document invariants: Use SAFETY: comments explaining preconditions
  3. Encapsulate: Don’t expose unsafe operations in public APIs
  4. Test thoroughly: Use Miri and comprehensive test coverage
  5. Validate inputs: Assert preconditions before unsafe operations

Unsafe Rust enables FFI, zero-cost abstractions, and performance optimizations where safety checks are insufficient.

Advanced Traits

Associated Types vs Generics

Associated types enforce single implementation per type:

trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

impl Iterator for Counter {
    type Item = u32;  // Only one Item type allowed
    // ...
}

Generics allow multiple implementations:

trait Iterator<T> {
    fn next(&mut self) -> Option<T>;
}
// Could implement Iterator<u32>, Iterator<String>, etc.

Default Generic Type Parameters

trait Add<Rhs = Self> {
    type Output;
    fn add(self, rhs: Rhs) -> Self::Output;
}

// Default: Point + Point
impl Add for Point {
    type Output = Point;
    fn add(self, other: Point) -> Point { /* ... */ }
}

// Custom: Point + Meters
impl Add<Meters> for Point {
    type Output = Point;
    fn add(self, other: Meters) -> Point { /* ... */ }
}

Use cases: Extending traits without breaking existing code, mixed-type operations.

Method Disambiguation

Multiple traits, same method names:

trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;
impl Pilot for Human {
    fn fly(&self) { println!("Flying a plane"); }
}
impl Wizard for Human {
    fn fly(&self) { println!("Flying with magic"); }
}

let person = Human;
Pilot::fly(&person);   // Explicit trait
Wizard::fly(&person);  // Explicit trait

Associated functions (no self):

println!("{}", <Dog as Animal>::baby_name());  // Fully qualified syntax

Supertraits

Require implementing one trait before another:

trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        println!("* {} *", self);  // Can use Display::fmt
    }
}

// Must implement Display before OutlinePrint
impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

Newtype Pattern

Circumvent orphan rule (implement external traits on external types):

struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

let w = Wrapper(vec![String::from("hello"), String::from("world")]);
println!("w = {}", w);

Additional benefits:

  • Type safety: UserId(u32) vs ProductId(u32)
  • Zero runtime cost
  • Controlled API surface

Access wrapped value with Deref:

impl Deref for Wrapper {
    type Target = Vec<String>;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

Advanced Types

Newtype Pattern for Type Safety

Compile-time type safety and abstraction:

struct UserId(u32);
struct ProductId(u32);

fn get_user(id: UserId) -> User { /* ... */ }
// get_user(ProductId(123)); // Compile error

Use cases: Units (Kilometers, Miles), IDs, domain modeling, API abstraction.

Type Aliases

Convenience syntax for complex types:

type Kilometers = i32;  // No type safety
type Thunk = Box<dyn Fn() + Send + 'static>;

// Result alias pattern (std::io uses this)
type Result<T> = std::result::Result<T, std::io::Error>;

fn write_data() -> Result<()> { /* ... */ }

Never Type (!)

Represents computations that never return:

fn never_returns() -> ! {
    panic!("This function never returns!");
}

// Key property: ! coerces to any type
let guess: u32 = match guess.trim().parse() {
    Ok(num) => num,
    Err(_) => continue,  // continue has type !, coerces to u32
};

Other ! expressions: panic!, loop {}, process::exit().

Dynamically Sized Types (DSTs)

Types with unknown compile-time size:

// str is a DST (not &str)
let s1: &str = "hello";      // OK: reference with size info
let s2: Box<str> = "hello".into();  // OK: smart pointer

// Won't compile - unknown size
// let s3: str = "hello";

Common DSTs: str, [T], dyn Trait.

Sized Trait

Generic functions implicitly require Sized:

fn generic<T>(t: T) {}           // Actually: fn generic<T: Sized>(t: T)
fn flexible<T: ?Sized>(t: &T) {} // ?Sized relaxes the bound

?Sized means “maybe sized” - requires using T behind a pointer (&T, Box<T>).

Advanced Functions and Closures

Function Pointers vs Closures

Function pointers (fn) are different from closure traits (Fn, FnMut, FnOnce):

fn add_one(x: i32) -> i32 { x + 1 }

fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
    f(arg) + f(arg)
}

let answer = do_twice(add_one, 5);  // Function pointer

Key differences:

  • fn is a type, not a trait
  • Function pointers implement all closure traits
  • Useful for FFI (C functions don’t have closures)

Practical Examples

Map with function vs closure:

let list = vec![1, 2, 3];
let list2: Vec<String> = list.iter().map(ToString::to_string).collect();

Enum constructors as function pointers:

enum Status { Value(u32) }
let list: Vec<Status> = (0u32..20).map(Status::Value).collect();

Returning Closures

Simple case with impl Trait:

fn returns_closure() -> impl Fn(i32) -> i32 {
    |x| x + 1
}

Multiple closures require trait objects (each closure has unique type):

fn returns_closure(condition: bool) -> Box<dyn Fn(i32) -> i32> {
    if condition {
        Box::new(|x| x + 1)
    } else {
        Box::new(|x| x * 2)
    }
}

JavaScript/TypeScript Comparison

JavaScript: Functions are first-class objects with uniform representation:

function add(x) { return x + 1; }
const multiply = (x) => x * 2;
const funcs = [add, multiply];  // Same type

Rust: Functions and closures have distinct types based on captures:

fn add(x: i32) -> i32 { x + 1 }
let multiplier = 2;
let multiply = |x| x * multiplier;  // Different type due to capture

// Need trait objects for heterogeneous storage
let funcs: Vec<Box<dyn Fn(i32) -> i32>> = vec![
    Box::new(add),
    Box::new(multiply),
];

Rust’s approach enables zero-cost abstractions - closures only pay for what they capture.

Macros

Compile-time metaprogramming through declarative and procedural macros.

Macros vs Functions

Macros: Variable parameters, operate on tokens, generate code at compile-time Functions: Fixed parameters, runtime execution, typed arguments

Declarative Macros (macro_rules!)

Pattern match on code structure:

macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}

Pattern syntax:

  • $x:expr captures expressions
  • $( ... ),* captures repeated patterns with , separator
  • $()* repeats code for each match

Procedural Macros

Transform TokenStreamTokenStream. Three types:

1. Custom derive Macros

#[derive(HelloMacro)]
struct Pancakes;

fn main() {
    Pancakes::hello_macro();
}

Implementation:

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    let ast = syn::parse(input).unwrap();
    impl_hello_macro(&ast)
}

2. Attribute-like Macros

#[route(GET, "/")]
fn index() {}

Signature:

#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream

3. Function-like Macros

let sql = sql!(SELECT * FROM posts WHERE id=1);

Signature:

#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream

Key Crates

  • syn: Parse Rust code into AST
  • quote: Generate Rust code from templates
  • proc_macro: Compiler API for token manipulation

Practical Applications

Declarative: Domain-specific syntax, boilerplate reduction, type-safe builders Procedural: ORM derive macros, framework attributes, compile-time validation

Performance: All expansion happens at compile-time with zero runtime cost.

Final Project: Building a Multithreaded Web Server

Build a basic HTTP server from scratch demonstrating systems programming concepts.

hello from rust

Project Components

  • TCP connection handling and HTTP request/response parsing
  • Thread pool implementation for concurrent request processing
  • Low-level networking without frameworks

Production note: Use hyper, warp, or axum for real applications. Implementation note: We use threads rather than async/await to focus on concurrency fundamentals, though many async runtimes employ thread pools internally.

Building a Single-Threaded Web Server

TCP Connection Handling

use std::net::TcpListener;

let listener = TcpListener::bind("127.0.0.1:7878")?;
for stream in listener.incoming() {
    let stream = stream?;
    handle_connection(stream);
}

Similar to Node.js server.listen(), but blocking by default.

HTTP Request Structure

GET / HTTP/1.1
Host: 127.0.0.1:7878
User-Agent: Mozilla/5.0...

[optional body]
  • Method + URI + Version
  • Headers until empty line
  • Optional body

Basic Response

fn handle_connection(mut stream: TcpStream) {
    let mut buffer = [0; 1024];
    stream.read(&mut buffer)?;
    
    let response = "HTTP/1.1 200 OK\r\n\r\nHello, World!";
    stream.write(response.as_bytes())?;
    stream.flush()?;
}

Serving Files

fn handle_connection(mut stream: TcpStream) {
    let mut buffer = [0; 1024];
    stream.read(&mut buffer)?;
    
    let get = b"GET / HTTP/1.1\r\n";
    let (status_line, filename) = if buffer.starts_with(get) {
        ("HTTP/1.1 200 OK", "hello.html")
    } else {
        ("HTTP/1.1 404 NOT FOUND", "404.html")
    };
    
    let contents = fs::read_to_string(filename)?;
    let response = format!(
        "{}\r\nContent-Length: {}\r\n\r\n{}",
        status_line,
        contents.len(),
        contents
    );
    
    stream.write(response.as_bytes())?;
    stream.flush()?;
}

Performance Limitation

// Simulate slow endpoint
if buffer.starts_with(b"GET /sleep HTTP/1.1\r\n") {
    thread::sleep(Duration::from_secs(5));
}

Problem: Single-threaded blocking - all requests wait for slow operations.

Node.js difference: Event loop handles I/O asynchronously on single thread, while this blocks the entire server.

Next: Thread pool for concurrent request handling.

Turning Our Single-Threaded Server into a Multithreaded Server

Problem: Sequential Request Processing

The current server blocks on each request:

// /sleep endpoint demonstrates the blocking issue
if buffer.starts_with(b"GET /sleep HTTP/1.1\r\n") {
    thread::sleep(Duration::from_secs(5));
}

Issue: All requests wait behind slow operations. Unlike Node.js’s event loop or async frameworks, this blocks the entire server.

Solution: Thread Pool Pattern

Thread pool benefits:

  • Fixed number of worker threads (prevents DoS via thread exhaustion)
  • Work queue distributes tasks across available workers
  • Graceful handling of concurrent requests

Implementation

1. Basic Threading (Naive Approach)

use std::thread;

for stream in listener.incoming() {
    let stream = stream.unwrap();
    thread::spawn(|| {
        handle_connection(stream);
    });
}

Problem: Unlimited thread creation can exhaust system resources.

2. ThreadPool API Design

let pool = ThreadPool::new(4);

for stream in listener.incoming() {
    let stream = stream.unwrap();
    pool.execute(|| {
        handle_connection(stream);
    });
}

3. ThreadPool Implementation

Core structure:

use std::sync::{mpsc, Arc, Mutex};

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: Option<mpsc::Sender<Job>>,
}

struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}

type Job = Box<dyn FnOnce() + Send + 'static>;

Constructor with validation:

impl ThreadPool {
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);
        
        let (sender, receiver) = mpsc::channel();
        let receiver = Arc::new(Mutex::new(receiver));
        let mut workers = Vec::with_capacity(size);
        
        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }
        
        ThreadPool {
            workers,
            sender: Some(sender),
        }
    }
}

Job execution:

impl ThreadPool {
    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);
        self.sender.as_ref().unwrap().send(job).unwrap();
    }
}

Worker implementation:

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || loop {
            let job = receiver.lock().unwrap().recv().unwrap();
            println!("Worker {id} got a job; executing.");
            job();
        });
        
        Worker { id, thread }
    }
}

Key Architecture Patterns

Channel-based work distribution:

  • ThreadPool holds sender
  • Workers share receiver via Arc<Mutex<Receiver>>
  • Mutex ensures single worker processes each job

Type safety:

  • Job type alias for boxed closures
  • Send + 'static bounds ensure thread safety
  • FnOnce trait for single execution

Resource management:

  • Pre-allocated worker threads
  • Bounded concurrency (DoS protection)
  • Work stealing via shared queue

Performance Characteristics

vs Single-threaded: Concurrent request handling vs Unlimited threading: Bounded resource usage vs Async I/O: More memory per connection, but simpler mental model vs Node.js: Multiple OS threads vs single-threaded event loop

Note: Modern async runtimes (tokio, async-std) often use similar thread pool patterns internally for CPU-bound work.

Graceful Shutdown and Cleanup

Problem: Resource Cleanup

Current thread pool implementation doesn’t clean up resources when dropping. Worker threads continue running indefinitely, preventing graceful shutdown.

Solution: Implement Drop Trait

1. Basic Drop Implementation

impl Drop for ThreadPool {
    fn drop(&mut self) {
        for worker in &mut self.workers {
            println!("Shutting down worker {}", worker.id);
            if let Some(thread) = worker.thread.take() {
                thread.join().unwrap();
            }
        }
    }
}

Problem: Workers are blocked on recv() calls - join() waits indefinitely.

2. Signaling Shutdown

Close channel to signal workers:

impl Drop for ThreadPool {
    fn drop(&mut self) {
        // Drop sender to close channel
        drop(self.sender.take());
        
        // Wait for workers to finish
        for worker in &mut self.workers {
            println!("Shutting down worker {}", worker.id);
            if let Some(thread) = worker.thread.take() {
                thread.join().unwrap();
            }
        }
    }
}

Update Worker to handle channel closure:

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || loop {
            let message = receiver.lock().unwrap().recv();
            
            match message {
                Ok(job) => {
                    println!("Worker {id} got a job; executing.");
                    job();
                }
                Err(_) => {
                    println!("Worker {id} disconnected; shutting down.");
                    break;
                }
            }
        });
        
        Worker { 
            id, 
            thread: Some(thread) 
        }
    }
}

3. Updated Data Structures

ThreadPool with Optional sender:

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: Option<mpsc::Sender<Job>>,
}

Worker with Optional thread:

struct Worker {
    id: usize,
    thread: Option<thread::JoinHandle<()>>,
}

Complete Implementation

ThreadPool:

impl ThreadPool {
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);
        
        let (sender, receiver) = mpsc::channel();
        let receiver = Arc::new(Mutex::new(receiver));
        let mut workers = Vec::with_capacity(size);
        
        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }
        
        ThreadPool {
            workers,
            sender: Some(sender),
        }
    }
    
    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);
        self.sender.as_ref().unwrap().send(job).unwrap();
    }
}

impl Drop for ThreadPool {
    fn drop(&mut self) {
        drop(self.sender.take());
        
        for worker in &mut self.workers {
            println!("Shutting down worker {}", worker.id);
            if let Some(thread) = worker.thread.take() {
                thread.join().unwrap();
            }
        }
    }
}

Testing Graceful Shutdown

Limited server for testing:

let pool = ThreadPool::new(4);

for stream in listener.incoming().take(2) {
    let stream = stream.unwrap();
    pool.execute(|| {
        handle_connection(stream);
    });
}

println!("Shutting down.");

Expected output:

Worker 0 got a job; executing.
Worker 1 got a job; executing.
Shutting down.
Shutting down worker 0
Worker 0 disconnected; shutting down.
Worker 1 disconnected; shutting down.
Shutting down worker 1
...

Key Patterns

Resource cleanup sequence:

  1. Signal shutdown (close channel)
  2. Wait for workers to finish current tasks
  3. Join all worker threads

Channel closure semantics:

  • Dropping sender closes channel
  • recv() returns Err when channel closed
  • Workers exit loop on channel closure

RAII (Resource Acquisition Is Initialization):

  • Drop trait ensures cleanup on scope exit
  • Automatic resource management
  • Exception safety through destructors

Production considerations:

  • Timeout for join operations
  • Graceful vs forceful shutdown
  • Signal handling for shutdown triggers

Appendix

The following sections contain reference material you may find useful in your Rust journey.

Appendix A: Keywords

The following list contains keywords that are reserved for current or future use by the Rust language. As such, they cannot be used as identifiers (except as raw identifiers as we’ll discuss in the “Raw Identifiers” section). Identifiers are names of functions, variables, parameters, struct fields, modules, crates, constants, macros, static values, attributes, types, traits, or lifetimes.

Keywords Currently in Use

The following is a list of keywords currently in use, with their functionality described.

  • as - perform primitive casting, disambiguate the specific trait containing an item, or rename items in use statements
  • async - return a Future instead of blocking the current thread
  • await - suspend execution until the result of a Future is ready
  • break - exit a loop immediately
  • const - define constant items or constant raw pointers
  • continue - continue to the next loop iteration
  • crate - in a module path, refers to the crate root
  • dyn - dynamic dispatch to a trait object
  • else - fallback for if and if let control flow constructs
  • enum - define an enumeration
  • extern - link an external function or variable
  • false - Boolean false literal
  • fn - define a function or the function pointer type
  • for - loop over items from an iterator, implement a trait, or specify a higher-ranked lifetime
  • if - branch based on the result of a conditional expression
  • impl - implement inherent or trait functionality
  • in - part of for loop syntax
  • let - bind a variable
  • loop - loop unconditionally
  • match - match a value to patterns
  • mod - define a module
  • move - make a closure take ownership of all its captures
  • mut - denote mutability in references, raw pointers, or pattern bindings
  • pub - denote public visibility in struct fields, impl blocks, or modules
  • ref - bind by reference
  • return - return from function
  • Self - a type alias for the type we are defining or implementing
  • self - method subject or current module
  • static - global variable or lifetime lasting the entire program execution
  • struct - define a structure
  • super - parent module of the current module
  • trait - define a trait
  • true - Boolean true literal
  • type - define a type alias or associated type
  • union - define a union; is only a keyword when used in a union declaration
  • unsafe - denote unsafe code, functions, traits, or implementations
  • use - bring symbols into scope; specify precise captures for generic and lifetime bounds
  • where - denote clauses that constrain a type
  • while - loop conditionally based on the result of an expression

Keywords Reserved for Future Use

The following keywords do not yet have any functionality but are reserved by Rust for potential future use.

  • abstract
  • become
  • box
  • do
  • final
  • gen
  • macro
  • override
  • priv
  • try
  • typeof
  • unsized
  • virtual
  • yield

Raw Identifiers

Raw identifiers are the syntax that lets you use keywords where they wouldn’t normally be allowed. You use a raw identifier by prefixing a keyword with r#.

For example, match is a keyword. If you try to compile the following function that uses match as its name:

Filename: src/main.rs

fn match(needle: &str, haystack: &str) -> bool {
    haystack.contains(needle)
}

you’ll get this error:

error: expected identifier, found keyword `match`
 --> src/main.rs:4:4
  |
4 | fn match(needle: &str, haystack: &str) -> bool {
  |    ^^^^^ expected identifier, found keyword

The error shows that you can’t use the keyword match as the function identifier. To use match as a function name, you need to use the raw identifier syntax, like this:

Filename: src/main.rs

fn r#match(needle: &str, haystack: &str) -> bool {
    haystack.contains(needle)
}

fn main() {
    assert!(r#match("foo", "foobar"));
}

This code will compile without any errors. Note the r# prefix on the function name in its definition as well as where the function is called in main.

Raw identifiers allow you to use any word you choose as an identifier, even if that word happens to be a reserved keyword. This gives us more freedom to choose identifier names, as well as lets us integrate with programs written in a language where these words aren’t keywords. In addition, raw identifiers allow you to use libraries written in a different Rust edition than your crate uses. For example, try isn’t a keyword in the 2015 edition but is in the 2018, 2021, and 2024 editions. If you depend on a library that is written using the 2015 edition and has a try function, you’ll need to use the raw identifier syntax, r#try in this case, to call that function from your code on later editions. See Appendix E for more information on editions.

Appendix B: Operators and Symbols

This appendix contains a glossary of Rust’s syntax, including operators and other symbols that appear by themselves or in the context of paths, generics, trait bounds, macros, attributes, comments, tuples, and brackets.

Operators

Table B-1 contains the operators in Rust, an example of how the operator would appear in context, a short explanation, and whether that operator is overloadable. If an operator is overloadable, the relevant trait to use to overload that operator is listed.

Table B-1: Operators

OperatorExampleExplanationOverloadable?
!ident!(...), ident!{...}, ident![...]Macro expansion
!!exprBitwise or logical complementNot
!=expr != exprNonequality comparisonPartialEq
%expr % exprArithmetic remainderRem
%=var %= exprArithmetic remainder and assignmentRemAssign
&&expr, &mut exprBorrow
&&type, &mut type, &'a type, &'a mut typeBorrowed pointer type
&expr & exprBitwise ANDBitAnd
&=var &= exprBitwise AND and assignmentBitAndAssign
&&expr && exprShort-circuiting logical AND
*expr * exprArithmetic multiplicationMul
*=var *= exprArithmetic multiplication and assignmentMulAssign
**exprDereferenceDeref
**const type, *mut typeRaw pointer
+trait + trait, 'a + traitCompound type constraint
+expr + exprArithmetic additionAdd
+=var += exprArithmetic addition and assignmentAddAssign
,expr, exprArgument and element separator
-- exprArithmetic negationNeg
-expr - exprArithmetic subtractionSub
-=var -= exprArithmetic subtraction and assignmentSubAssign
->fn(...) -> type, |…| -> typeFunction and closure return type
.expr.identField access
.expr.ident(expr, ...)Method call
.expr.0, expr.1, etc.Tuple indexing
...., expr.., ..expr, expr..exprRight-exclusive range literalPartialOrd
..=..=expr, expr..=exprRight-inclusive range literalPartialOrd
....exprStruct literal update syntax
..variant(x, ..), struct_type { x, .. }“And the rest” pattern binding
...expr...expr(Deprecated, use ..= instead) In a pattern: inclusive range pattern
/expr / exprArithmetic divisionDiv
/=var /= exprArithmetic division and assignmentDivAssign
:pat: type, ident: typeConstraints
:ident: exprStruct field initializer
:'a: loop {...}Loop label
;expr;Statement and item terminator
;[...; len]Part of fixed-size array syntax
<<expr << exprLeft-shiftShl
<<=var <<= exprLeft-shift and assignmentShlAssign
<expr < exprLess than comparisonPartialOrd
<=expr <= exprLess than or equal to comparisonPartialOrd
=var = expr, ident = typeAssignment/equivalence
==expr == exprEquality comparisonPartialEq
=>pat => exprPart of match arm syntax
>expr > exprGreater than comparisonPartialOrd
>=expr >= exprGreater than or equal to comparisonPartialOrd
>>expr >> exprRight-shiftShr
>>=var >>= exprRight-shift and assignmentShrAssign
@ident @ patPattern binding
^expr ^ exprBitwise exclusive ORBitXor
^=var ^= exprBitwise exclusive OR and assignmentBitXorAssign
|pat | patPattern alternatives
|expr | exprBitwise ORBitOr
|=var |= exprBitwise OR and assignmentBitOrAssign
||expr || exprShort-circuiting logical OR
?expr?Error propagation

Non-operator Symbols

The following list contains all symbols that don’t function as operators; that is, they don’t behave like a function or method call.

Table B-2 shows symbols that appear on their own and are valid in a variety of locations.

Table B-2: Stand-Alone Syntax

SymbolExplanation
'identNamed lifetime or loop label
...u8, ...i32, ...f64, ...usize, etc.Numeric literal of specific type
"..."String literal
r"...", r#"..."#, r##"..."##, etc.Raw string literal, escape characters not processed
b"..."Byte string literal; constructs an array of bytes instead of a string
br"...", br#"..."#, br##"..."##, etc.Raw byte string literal, combination of raw and byte string literal
'...'Character literal
b'...'ASCII byte literal
|…| exprClosure
!Always empty bottom type for diverging functions
_“Ignored” pattern binding; also used to make integer literals readable

Table B-3 shows symbols that appear in the context of a path through the module hierarchy to an item.

Table B-3: Path-Related Syntax

SymbolExplanation
ident::identNamespace path
::pathPath relative to the extern prelude, where all other crates are rooted (i.e., an explicitly absolute path including crate name)
self::pathPath relative to the current module (i.e., an explicitly relative path).
super::pathPath relative to the parent of the current module
type::ident, <type as trait>::identAssociated constants, functions, and types
<type>::...Associated item for a type that cannot be directly named (e.g., <&T>::..., <[T]>::..., etc.)
trait::method(...)Disambiguating a method call by naming the trait that defines it
type::method(...)Disambiguating a method call by naming the type for which it’s defined
<type as trait>::method(...)Disambiguating a method call by naming the trait and type

Table B-4 shows symbols that appear in the context of using generic type parameters.

Table B-4: Generics

SymbolExplanation
path<...>Specifies parameters to generic type in a type (e.g., Vec<u8>)
path::<...>, method::<...>Specifies parameters to generic type, function, or method in an expression; often referred to as turbofish (e.g., "42".parse::<i32>())
fn ident<...> ...Define generic function
struct ident<...> ...Define generic structure
enum ident<...> ...Define generic enumeration
impl<...> ...Define generic implementation
for<...> typeHigher-ranked lifetime bounds
type<ident=type>A generic type where one or more associated types have specific assignments (e.g., Iterator<Item=T>)

Table B-5 shows symbols that appear in the context of constraining generic type parameters with trait bounds.

Table B-5: Trait Bound Constraints

SymbolExplanation
T: UGeneric parameter T constrained to types that implement U
T: 'aGeneric type T must outlive lifetime 'a (meaning the type cannot transitively contain any references with lifetimes shorter than 'a)
T: 'staticGeneric type T contains no borrowed references other than 'static ones
'b: 'aGeneric lifetime 'b must outlive lifetime 'a
T: ?SizedAllow generic type parameter to be a dynamically sized type
'a + trait, trait + traitCompound type constraint

Table B-6 shows symbols that appear in the context of calling or defining macros and specifying attributes on an item.

Table B-6: Macros and Attributes

SymbolExplanation
#[meta]Outer attribute
#![meta]Inner attribute
$identMacro substitution
$ident:kindMacro capture
$(…)…Macro repetition
ident!(...), ident!{...}, ident![...]Macro invocation

Table B-7 shows symbols that create comments.

Table B-7: Comments

SymbolExplanation
//Line comment
//!Inner line doc comment
///Outer line doc comment
/*...*/Block comment
/*!...*/Inner block doc comment
/**...*/Outer block doc comment

Table B-8 shows the contexts in which parentheses are used.

Table B-8: Parentheses

SymbolExplanation
()Empty tuple (aka unit), both literal and type
(expr)Parenthesized expression
(expr,)Single-element tuple expression
(type,)Single-element tuple type
(expr, ...)Tuple expression
(type, ...)Tuple type
expr(expr, ...)Function call expression; also used to initialize tuple structs and tuple enum variants

Table B-9 shows the contexts in which curly braces are used.

Table B-9: Curly Brackets

ContextExplanation
{...}Block expression
Type {...}struct literal

Table B-10 shows the contexts in which square brackets are used.

Table B-10: Square Brackets

ContextExplanation
[...]Array literal
[expr; len]Array literal containing len copies of expr
[type; len]Array type containing len instances of type
expr[expr]Collection indexing. Overloadable (Index, IndexMut)
expr[..], expr[a..], expr[..b], expr[a..b]Collection indexing pretending to be collection slicing, using Range, RangeFrom, RangeTo, or RangeFull as the “index”

Appendix C: Derivable Traits

In various places in the book, we’ve discussed the derive attribute, which you can apply to a struct or enum definition. The derive attribute generates code that will implement a trait with its own default implementation on the type you’ve annotated with the derive syntax.

In this appendix, we provide a reference of all the traits in the standard library that you can use with derive. Each section covers:

  • What operators and methods deriving this trait will enable
  • What the implementation of the trait provided by derive does
  • What implementing the trait signifies about the type
  • The conditions in which you’re allowed or not allowed to implement the trait
  • Examples of operations that require the trait

If you want different behavior from that provided by the derive attribute, consult the standard library documentation for each trait for details on how to manually implement them.

The traits listed here are the only ones defined by the standard library that can be implemented on your types using derive. Other traits defined in the standard library don’t have sensible default behavior, so it’s up to you to implement them in the way that makes sense for what you’re trying to accomplish.

An example of a trait that can’t be derived is Display, which handles formatting for end users. You should always consider the appropriate way to display a type to an end user. What parts of the type should an end user be allowed to see? What parts would they find relevant? What format of the data would be most relevant to them? The Rust compiler doesn’t have this insight, so it can’t provide appropriate default behavior for you.

The list of derivable traits provided in this appendix is not comprehensive: libraries can implement derive for their own traits, making the list of traits you can use derive with truly open-ended. Implementing derive involves using a procedural macro, which is covered in the “Macros” section of Chapter 20.

Debug for Programmer Output

The Debug trait enables debug formatting in format strings, which you indicate by adding :? within {} placeholders.

The Debug trait allows you to print instances of a type for debugging purposes, so you and other programmers using your type can inspect an instance at a particular point in a program’s execution.

The Debug trait is required, for example, in the use of the assert_eq! macro. This macro prints the values of instances given as arguments if the equality assertion fails so programmers can see why the two instances weren’t equal.

PartialEq and Eq for Equality Comparisons

The PartialEq trait allows you to compare instances of a type to check for equality and enables use of the == and != operators.

Deriving PartialEq implements the eq method. When PartialEq is derived on structs, two instances are equal only if all fields are equal, and the instances are not equal if any fields are not equal. When derived on enums, each variant is equal to itself and not equal to the other variants.

The PartialEq trait is required, for example, with the use of the assert_eq! macro, which needs to be able to compare two instances of a type for equality.

The Eq trait has no methods. Its purpose is to signal that for every value of the annotated type, the value is equal to itself. The Eq trait can only be applied to types that also implement PartialEq, although not all types that implement PartialEq can implement Eq. One example of this is floating point number types: the implementation of floating point numbers states that two instances of the not-a-number (NaN) value are not equal to each other.

An example of when Eq is required is for keys in a HashMap<K, V> so the HashMap<K, V> can tell whether two keys are the same.

PartialOrd and Ord for Ordering Comparisons

The PartialOrd trait allows you to compare instances of a type for sorting purposes. A type that implements PartialOrd can be used with the <, >, <=, and >= operators. You can only apply the PartialOrd trait to types that also implement PartialEq.

Deriving PartialOrd implements the partial_cmp method, which returns an Option<Ordering> that will be None when the values given don’t produce an ordering. An example of a value that doesn’t produce an ordering, even though most values of that type can be compared, is the not-a-number (NaN) floating point value. Calling partial_cmp with any floating-point number and the NaN floating-point value will return None.

When derived on structs, PartialOrd compares two instances by comparing the value in each field in the order in which the fields appear in the struct definition. When derived on enums, variants of the enum declared earlier in the enum definition are considered less than the variants listed later.

The PartialOrd trait is required, for example, for the gen_range method from the rand crate that generates a random value in the range specified by a range expression.

The Ord trait allows you to know that for any two values of the annotated type, a valid ordering will exist. The Ord trait implements the cmp method, which returns an Ordering rather than an Option<Ordering> because a valid ordering will always be possible. You can only apply the Ord trait to types that also implement PartialOrd and Eq (and Eq requires PartialEq). When derived on structs and enums, cmp behaves the same way as the derived implementation for partial_cmp does with PartialOrd.

An example of when Ord is required is when storing values in a BTreeSet<T>, a data structure that stores data based on the sort order of the values.

Clone and Copy for Duplicating Values

The Clone trait allows you to explicitly create a deep copy of a value, and the duplication process might involve running arbitrary code and copying heap data. See Variables and Data Interacting with Clone” in Chapter 4 for more information on Clone.

Deriving Clone implements the clone method, which when implemented for the whole type, calls clone on each of the parts of the type. This means all the fields or values in the type must also implement Clone to derive Clone.

An example of when Clone is required is when calling the to_vec method on a slice. The slice doesn’t own the type instances it contains, but the vector returned from to_vec will need to own its instances, so to_vec calls clone on each item. Thus the type stored in the slice must implement Clone.

The Copy trait allows you to duplicate a value by only copying bits stored on the stack; no arbitrary code is necessary. See “Stack-Only Data: Copy” in Chapter 4 for more information on Copy.

The Copy trait doesn’t define any methods to prevent programmers from overloading those methods and violating the assumption that no arbitrary code is being run. That way, all programmers can assume that copying a value will be very fast.

You can derive Copy on any type whose parts all implement Copy. A type that implements Copy must also implement Clone, because a type that implements Copy has a trivial implementation of Clone that performs the same task as Copy.

The Copy trait is rarely required; types that implement Copy have optimizations available, meaning you don’t have to call clone, which makes the code more concise.

Everything possible with Copy you can also accomplish with Clone, but the code might be slower or have to use clone in places.

Hash for Mapping a Value to a Value of Fixed Size

The Hash trait allows you to take an instance of a type of arbitrary size and map that instance to a value of fixed size using a hash function. Deriving Hash implements the hash method. The derived implementation of the hash method combines the result of calling hash on each of the parts of the type, meaning all fields or values must also implement Hash to derive Hash.

An example of when Hash is required is in storing keys in a HashMap<K, V> to store data efficiently.

Default for Default Values

The Default trait allows you to create a default value for a type. Deriving Default implements the default function. The derived implementation of the default function calls the default function on each part of the type, meaning all fields or values in the type must also implement Default to derive Default.

The Default::default function is commonly used in combination with the struct update syntax discussed in “Creating Instances from Other Instances with Struct Update Syntax” in Chapter 5. You can customize a few fields of a struct and then set and use a default value for the rest of the fields by using ..Default::default().

The Default trait is required when you use the method unwrap_or_default on Option<T> instances, for example. If the Option<T> is None, the method unwrap_or_default will return the result of Default::default for the type T stored in the Option<T>.

Appendix D - Useful Development Tools

In this appendix, we talk about some useful development tools that the Rust project provides. We’ll look at automatic formatting, quick ways to apply warning fixes, a linter, and integrating with IDEs.

Automatic Formatting with rustfmt

The rustfmt tool reformats your code according to the community code style. Many collaborative projects use rustfmt to prevent arguments about which style to use when writing Rust: everyone formats their code using the tool.

Rust installations include rustfmt by default, so you should already have the programs rustfmt and cargo-fmt on your system. These two commands are analogous to rustc and cargo in that rustfmt allows finer-grained control and cargo-fmt understands conventions of a project that uses Cargo. To format any Cargo project, enter the following:

$ cargo fmt

Running this command reformats all the Rust code in the current crate. This should only change the code style, not the code semantics.

This command gives you rustfmt and cargo-fmt, similar to how Rust gives you both rustc and cargo. To format any Cargo project, enter the following:

$ cargo fmt

Running this command reformats all the Rust code in the current crate. This should only change the code style, not the code semantics. For more information on rustfmt, see its documentation.

Fix Your Code with rustfix

The rustfix tool is included with Rust installations and can automatically fix compiler warnings that have a clear way to correct the problem that’s likely what you want. It’s likely you’ve seen compiler warnings before. For example, consider this code:

Filename: src/main.rs

fn main() {
    let mut x = 42;
    println!("{x}");
}

Here, we’re defining the variable x as mutable, but we never actually mutate it. Rust warns us about that:

$ cargo build
   Compiling myprogram v0.1.0 (file:///projects/myprogram)
warning: variable does not need to be mutable
 --> src/main.rs:2:9
  |
2 |     let mut x = 0;
  |         ----^
  |         |
  |         help: remove this `mut`
  |
  = note: `#[warn(unused_mut)]` on by default

The warning suggests that we remove the mut keyword. We can automatically apply that suggestion using the rustfix tool by running the command cargo fix:

$ cargo fix
    Checking myprogram v0.1.0 (file:///projects/myprogram)
      Fixing src/main.rs (1 fix)
    Finished dev [unoptimized + debuginfo] target(s) in 0.59s

When we look at src/main.rs again, we’ll see that cargo fix has changed the code:

Filename: src/main.rs

fn main() {
    let x = 42;
    println!("{x}");
}

The x variable is now immutable, and the warning no longer appears.

You can also use the cargo fix command to transition your code between different Rust editions. Editions are covered in Appendix E.

More Lints with Clippy

The Clippy tool is a collection of lints to analyze your code so you can catch common mistakes and improve your Rust code. Clippy is included with standard Rust installations.

To run Clippy’s lints on any Cargo project, enter the following:

$ cargo clippy

For example, say you write a program that uses an approximation of a mathematical constant, such as pi, as this program does:

fn main() {
    let x = 3.1415;
    let r = 8.0;
    println!("the area of the circle is {}", x * r * r);
}

Running cargo clippy on this project results in this error:

error: approximate value of `f{32, 64}::consts::PI` found
 --> src/main.rs:2:13
  |
2 |     let x = 3.1415;
  |             ^^^^^^
  |
  = note: `#[deny(clippy::approx_constant)]` on by default
  = help: consider using the constant directly
  = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#approx_constant

This error lets you know that Rust already has a more precise PI constant defined, and that your program would be more correct if you used the constant instead. You would then change your code to use the PI constant. The following code doesn’t result in any errors or warnings from Clippy:

fn main() {
    let x = std::f64::consts::PI;
    let r = 8.0;
    println!("the area of the circle is {}", x * r * r);
}

For more information on Clippy, see its documentation.

IDE Integration Using rust-analyzer

To help IDE integration, the Rust community recommends using rust-analyzer. This tool is a set of compiler-centric utilities that speaks the Language Server Protocol, which is a specification for IDEs and programming languages to communicate with each other. Different clients can use rust-analyzer, such as the Rust analyzer plug-in for Visual Studio Code.

Visit the rust-analyzer project’s home page for installation instructions, then install the language server support in your particular IDE. Your IDE will gain abilities such as autocompletion, jump to definition, and inline errors.

Appendix E - Editions

In Chapter 1, you saw that cargo new adds a bit of metadata to your Cargo.toml file about an edition. This appendix talks about what that means!

The Rust language and compiler have a six-week release cycle, meaning users get a constant stream of new features. Other programming languages release larger changes less often; Rust releases smaller updates more frequently. After a while, all of these tiny changes add up. But from release to release, it can be difficult to look back and say, “Wow, between Rust 1.10 and Rust 1.31, Rust has changed a lot!”

Every three years or so, the Rust team produces a new Rust edition. Each edition brings together the features that have landed into a clear package with fully updated documentation and tooling. New editions ship as part of the usual six-week release process.

Editions serve different purposes for different people:

  • For active Rust users, a new edition brings together incremental changes into an easy-to-understand package.
  • For non-users, a new edition signals that some major advancements have landed, which might make Rust worth another look.
  • For those developing Rust, a new edition provides a rallying point for the project as a whole.

At the time of this writing, four Rust editions are available: Rust 2015, Rust 2018, Rust 2021, and Rust 2024. This book is written using Rust 2024 edition idioms.

The edition key in Cargo.toml indicates which edition the compiler should use for your code. If the key doesn’t exist, Rust uses 2015 as the edition value for backward compatibility reasons.

Each project can opt in to an edition other than the default 2015 edition. Editions can contain incompatible changes, such as including a new keyword that conflicts with identifiers in code. However, unless you opt in to those changes, your code will continue to compile even as you upgrade the Rust compiler version you use.

All Rust compiler versions support any edition that existed prior to that compiler’s release, and they can link crates of any supported editions together. Edition changes only affect the way the compiler initially parses code. Therefore, if you’re using Rust 2015 and one of your dependencies uses Rust 2018, your project will compile and be able to use that dependency. The opposite situation, where your project uses Rust 2018 and a dependency uses Rust 2015, works as well.

To be clear: most features will be available on all editions. Developers using any Rust edition will continue to see improvements as new stable releases are made. However, in some cases, mainly when new keywords are added, some new features might only be available in later editions. You will need to switch editions if you want to take advantage of such features.

For more details, the Edition Guide is a complete book about editions that enumerates the differences between editions and explains how to automatically upgrade your code to a new edition via cargo fix.

Appendix F: Translations of the Book

For resources in languages other than English. Most are still in progress; see the Translations label to help or let us know about a new translation!

Appendix G - How Rust is Made and “Nightly Rust”

This appendix is about how Rust is made and how that affects you as a Rust developer.

Stability Without Stagnation

As a language, Rust cares a lot about the stability of your code. We want Rust to be a rock-solid foundation you can build on, and if things were constantly changing, that would be impossible. At the same time, if we can’t experiment with new features, we may not find out important flaws until after their release, when we can no longer change things.

Our solution to this problem is what we call “stability without stagnation”, and our guiding principle is this: you should never have to fear upgrading to a new version of stable Rust. Each upgrade should be painless, but should also bring you new features, fewer bugs, and faster compile times.

Choo, Choo! Release Channels and Riding the Trains

Rust development operates on a train schedule. That is, all development is done on the master branch of the Rust repository. Releases follow a software release train model, which has been used by Cisco IOS and other software projects. There are three release channels for Rust:

  • Nightly
  • Beta
  • Stable

Most Rust developers primarily use the stable channel, but those who want to try out experimental new features may use nightly or beta.

Here’s an example of how the development and release process works: let’s assume that the Rust team is working on the release of Rust 1.5. That release happened in December of 2015, but it will provide us with realistic version numbers. A new feature is added to Rust: a new commit lands on the master branch. Each night, a new nightly version of Rust is produced. Every day is a release day, and these releases are created by our release infrastructure automatically. So as time passes, our releases look like this, once a night:

nightly: * - - * - - *

Every six weeks, it’s time to prepare a new release! The beta branch of the Rust repository branches off from the master branch used by nightly. Now, there are two releases:

nightly: * - - * - - *
                     |
beta:                *

Most Rust users do not use beta releases actively, but test against beta in their CI system to help Rust discover possible regressions. In the meantime, there’s still a nightly release every night:

nightly: * - - * - - * - - * - - *
                     |
beta:                *

Let’s say a regression is found. Good thing we had some time to test the beta release before the regression snuck into a stable release! The fix is applied to master, so that nightly is fixed, and then the fix is backported to the beta branch, and a new release of beta is produced:

nightly: * - - * - - * - - * - - * - - *
                     |
beta:                * - - - - - - - - *

Six weeks after the first beta was created, it’s time for a stable release! The stable branch is produced from the beta branch:

nightly: * - - * - - * - - * - - * - - * - * - *
                     |
beta:                * - - - - - - - - *
                                       |
stable:                                *

Hooray! Rust 1.5 is done! However, we’ve forgotten one thing: because the six weeks have gone by, we also need a new beta of the next version of Rust, 1.6. So after stable branches off of beta, the next version of beta branches off of nightly again:

nightly: * - - * - - * - - * - - * - - * - * - *
                     |                         |
beta:                * - - - - - - - - *       *
                                       |
stable:                                *

This is called the “train model” because every six weeks, a release “leaves the station”, but still has to take a journey through the beta channel before it arrives as a stable release.

Rust releases every six weeks, like clockwork. If you know the date of one Rust release, you can know the date of the next one: it’s six weeks later. A nice aspect of having releases scheduled every six weeks is that the next train is coming soon. If a feature happens to miss a particular release, there’s no need to worry: another one is happening in a short time! This helps reduce pressure to sneak possibly unpolished features in close to the release deadline.

Thanks to this process, you can always check out the next build of Rust and verify for yourself that it’s easy to upgrade to: if a beta release doesn’t work as expected, you can report it to the team and get it fixed before the next stable release happens! Breakage in a beta release is relatively rare, but rustc is still a piece of software, and bugs do exist.

Maintenance time

The Rust project supports the most recent stable version. When a new stable version is released, the old version reaches its end of life (EOL). This means each version is supported for six weeks.

Unstable Features

There’s one more catch with this release model: unstable features. Rust uses a technique called “feature flags” to determine what features are enabled in a given release. If a new feature is under active development, it lands on master, and therefore, in nightly, but behind a feature flag. If you, as a user, wish to try out the work-in-progress feature, you can, but you must be using a nightly release of Rust and annotate your source code with the appropriate flag to opt in.

If you’re using a beta or stable release of Rust, you can’t use any feature flags. This is the key that allows us to get practical use with new features before we declare them stable forever. Those who wish to opt into the bleeding edge can do so, and those who want a rock-solid experience can stick with stable and know that their code won’t break. Stability without stagnation.

This book only contains information about stable features, as in-progress features are still changing, and surely they’ll be different between when this book was written and when they get enabled in stable builds. You can find documentation for nightly-only features online.

Rustup and the Role of Rust Nightly

Rustup makes it easy to change between different release channels of Rust, on a global or per-project basis. By default, you’ll have stable Rust installed. To install nightly, for example:

$ rustup toolchain install nightly

You can see all of the toolchains (releases of Rust and associated components) you have installed with rustup as well. Here’s an example on one of your authors’ Windows computer:

> rustup toolchain list
stable-x86_64-pc-windows-msvc (default)
beta-x86_64-pc-windows-msvc
nightly-x86_64-pc-windows-msvc

As you can see, the stable toolchain is the default. Most Rust users use stable most of the time. You might want to use stable most of the time, but use nightly on a specific project, because you care about a cutting-edge feature. To do so, you can use rustup override in that project’s directory to set the nightly toolchain as the one rustup should use when you’re in that directory:

$ cd ~/projects/needs-nightly
$ rustup override set nightly

Now, every time you call rustc or cargo inside of ~/projects/needs-nightly, rustup will make sure that you are using nightly Rust, rather than your default of stable Rust. This comes in handy when you have a lot of Rust projects!

The RFC Process and Teams

So how do you learn about these new features? Rust’s development model follows a Request For Comments (RFC) process. If you’d like an improvement in Rust, you can write up a proposal, called an RFC.

Anyone can write RFCs to improve Rust, and the proposals are reviewed and discussed by the Rust team, which is comprised of many topic subteams. There’s a full list of the teams on Rust’s website, which includes teams for each area of the project: language design, compiler implementation, infrastructure, documentation, and more. The appropriate team reads the proposal and the comments, writes some comments of their own, and eventually, there’s consensus to accept or reject the feature.

If the feature is accepted, an issue is opened on the Rust repository, and someone can implement it. The person who implements it very well may not be the person who proposed the feature in the first place! When the implementation is ready, it lands on the master branch behind a feature gate, as we discussed in the “Unstable Features” section.

After some time, once Rust developers who use nightly releases have been able to try out the new feature, team members will discuss the feature, how it’s worked out on nightly, and decide if it should make it into stable Rust or not. If the decision is to move forward, the feature gate is removed, and the feature is now considered stable! It rides the trains into a new stable release of Rust.