Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Preface

This book teaches Rust to someone who already programs. If you have written loops and functions in Lux, Python, JavaScript, or anything of that family, you have what you need. You will not be told what a variable is. You will be told what Rust does with one, which is a different and more interesting question.

Rust asks more of you than a scripting language does, and gives more back. Two things will be new. The first is that types are checked before the program runs rather than while it runs, so a class of mistake that a scripting language reports at three in the morning is reported by the compiler at your desk. The second is ownership: Rust tracks, at compile time, which part of your program is responsible for each piece of memory. That is the part nobody arrives already knowing, and Chapter 5 is where it gets its due. Everything before that chapter is written so you can reach it without surprises.

The pace is brisk on purpose. Programs come first and explanation follows, and the explanation stops when the point has been made. A feature that you can already guess the shape of gets a sentence. A feature that will bite you gets a page. If you want the complete description of some corner of the language, the official Rust Programming Language and the Rust Reference are free online and this book is not trying to replace them. What this book is trying to do is get you writing real Rust in a week rather than a month.

Read it with a keyboard. Type the programs in, run them, then break them and read what the compiler says. Rust’s error messages are the best part of the tooling, and learning to read them is learning the language. The exercises are short and most of them are variations on the program just presented; do the ones that look interesting and skip the rest without guilt.

Getting the compiler

Install Rust from https://rustup.rs. That gives you rustc, the compiler, and cargo, which manages projects and dependencies and is how you will do nearly everything. Check the install with cargo --version.

The code here is Rust 2021 edition. The newest thing it uses is let ... else, so any compiler from 2023 onwards has everything. Where a program’s output depends on your machine, the text says so.

About the examples

Every complete program in this book lives in the examples directory beside the text, one crate per chapter. From examples, running

cargo run -p ch01 --bin fahr2

builds and runs the program named fahr2 from Chapter 1. The programs are the ones printed here, not adaptations, so if something does not behave the way the text claims, the text is wrong and you should trust the compiler.

The programs that deliberately fail to compile live in examples/errors, one per mistake. Every compiler message quoted in this book came from running rustc on the file of that name, and Appendix C collects them.

A note if you are coming from Lux

Lux already taught you most of Rust’s data model without naming it. Values are immutable unless you say otherwise, there is no null, a function that might fail hands back an Option or a Result, and match has to cover every case. All four of those are Rust, and they are the parts of Rust that people coming from Python find strangest. You are further along than you think. Appendix B maps the two languages construct by construct; read it now if you want the shortcut, or later when something looks familiar and you want to check.

1. A Tutorial Introduction

This chapter is a tour. It moves fast, shows real programs, and leaves the careful definitions for later chapters. The goal is to get enough of Rust into your hands that the rest of the book has something to build on. Things you do not fully understand here are explained properly further along, and the text says where.

1.1 Getting Started

The only way to start.

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

Put that in a file called hello.rs and compile it with rustc hello.rs, which produces an executable called hello. Run it and you get one line of output.

In practice nobody compiles single files for long. Cargo makes a project:

cargo new hello
cd hello
cargo run

cargo new writes a directory containing Cargo.toml, which names the project and lists its dependencies, and src/main.rs, which contains the program above. cargo run compiles and runs it. cargo build compiles without running, and cargo build --release compiles with optimizations, much slower to compile and much faster to run. Use the plain form while you work.

The program itself is four tokens’ worth of ideas. fn introduces a function. main is the one the system calls to start the program. The parentheses hold parameters, of which there are none here. The braces hold the body.

println! prints a line. The exclamation mark says it is a macro rather than a function, which for now means only that it can do things a function cannot, such as check the format string against the arguments while compiling. You will type it constantly and think about it never.

Exercise 1-1. Run the program. Then remove the exclamation mark from println and compile again. Read the error.

Exercise 1-2. Print several lines. Find out what \n and \t do inside the quotes, and what \\ and {{ are for.

1.2 Variables and Arithmetic Expressions

The next program prints a table of Fahrenheit temperatures and their Celsius equivalents, using the formula C = (5/9)(F−32).

// Print a Fahrenheit-Celsius table for fahr = 0, 20, ..., 300.
fn main() {
    let lower = 0;
    let upper = 300;
    let step = 20;

    let mut fahr = lower;
    while fahr <= upper {
        let celsius = 5 * (fahr - 32) / 9;
        println!("{fahr}\t{celsius}");
        fahr += step;
    }
}

It prints:

0	-17
20	-6
40	4
...
300	148

Comments run from // to the end of the line. /* ... */ also works and nests properly, but the line form is what Rust programmers use.

let introduces a variable. Notice there are no type declarations: Rust works out that these are integers from the values they are given, and settles on i32, a 32-bit signed integer, as its default choice. You can say the type when you want to, and sometimes you must:

let upper: i64 = 300;

The word mut is the interesting one. A let binding is immutable by default, so fahr has to be declared let mut because the loop assigns to it. The other three never change and so do not carry it. This is backwards from most languages, where anything is assignable unless you take steps, and it is the first small example of Rust’s general habit: the safe, restricted thing is what you get for free, and the flexible thing costs a word. If you forget, the compiler is specific about it.

error[E0384]: cannot assign twice to immutable variable `fahr`
 --> assign_twice.rs:3:5
  |
2 |     let fahr = 0;
  |         ---- first assignment to `fahr`
3 |     fahr = fahr + 20;
  |     ^^^^^^^^^^^^^^^^ cannot assign twice to immutable variable
  |
help: consider making this binding mutable
  |
2 |     let mut fahr = 0;
  |         +++

Read compiler output from the top. The first line gives the error code and the one-sentence summary, the middle shows the source with the relevant spans marked, and the help at the bottom is often the literal fix. rustc --explain E0384 prints a page about any code you see.

The while loop is what you expect, except that the condition needs no parentheses and the braces are not optional. fahr += step adds and assigns, as in C. There is no ++.

Inside the format string, {fahr} prints the variable of that name. You can also write {} and pass the value as an argument, needed for anything that is not a plain variable:

println!("{} {}", fahr, 5 * (fahr - 32) / 9);

Now the arithmetic, where this program lies to you. 5 * (fahr - 32) / 9 is integer arithmetic throughout, so the division truncates toward zero and the table is wrong in the last digit. C has the same trap and K&R spends the same paragraph on it. The fix is floating point.

// Same table, floating point, right-aligned columns.
fn main() {
    let lower = 0.0;
    let upper = 300.0;
    let step = 20.0;

    let mut fahr = lower;
    while fahr <= upper {
        let celsius = (fahr - 32.0) * 5.0 / 9.0;
        println!("{fahr:3.0} {celsius:6.1}");
        fahr += step;
    }
}
  0  -17.8
 20   -6.7
 40    4.4
 60   15.6
 80   26.7
100   37.8

A literal with a decimal point is f64, a 64-bit float. {fahr:3.0} means a minimum width of three characters and zero digits after the point; {celsius:6.1} means width six, one digit. Width pads, it does not truncate.

Rust does not convert between number types on its own. Write 5 * (fahr - 32.0) with fahr a float and the compiler stops you with cannot multiply {integer} by {float}. In a scripting language that mixture is silently fine, and in C it is silently something, and in Rust it is a compile error with the position of the offending operator underlined. Conversions are written with as, which Chapter 2 covers.

Exercise 1-3. Add a heading to the table.

Exercise 1-4. Print the table in reverse order, from 300 degrees to 0.

Exercise 1-5. Write a program that converts Celsius to Fahrenheit.

1.3 The for Statement

The same table again, written the way a Rust programmer would write it:

// Same table again, as a for loop over a range.
fn main() {
    for i in 0..=15 {
        let fahr = f64::from(i) * 20.0;
        println!("{fahr:3.0} {:6.1}", (fahr - 32.0) * 5.0 / 9.0);
    }
}

0..=15 is a range that includes both ends. 0..16 is the same range with the top end excluded, and the exclusive form is the one you will use most, because 0..v.len() is the natural way to talk about the positions in a collection of v.len() things.

Rust’s for is not C’s. It does not have three clauses; it walks something. A range is one such something, and so is an array, a vector, a hash map, or the lines of a file. This is the same for you know from Python, with the same comfortable property that the loop cannot run off the end of the thing it is walking.

f64::from(i) converts the integer i to a float. The double colon is a path separator, the way a dot is in some languages: it names something inside something else, here the from function belonging to the type f64.

Exercise 1-6. Rewrite the temperature table with a for loop over a range of Fahrenheit values rather than a count, using .step_by(20).

1.4 Constants

Magic numbers in the middle of a program are as bad in Rust as anywhere else.

const LOWER: i32 = 0;
const UPPER: i32 = 300;
const STEP: i32 = 20;

fn main() {
    for fahr in (LOWER..=UPPER).step_by(STEP as usize) {
        println!("{fahr}");
    }
}

A const must have its type written out and its value must be computable at compile time. By convention constants are named in capitals with underscores. They can live outside any function, as here, and that is how you make something visible to the whole file.

1.5 Input and Output

Here is a program that copies its input to its output, one byte at a time.

// Copy input to output, one byte at a time.
use std::io::{self, Read, Write};

fn main() {
    let mut input = io::stdin().lock();
    let mut output = io::stdout().lock();
    let mut byte = [0u8; 1];

    while let Ok(1) = input.read(&mut byte) {
        output.write_all(&byte).unwrap();
    }
}

Run it with cargo run -p ch01 --bin copy < somefile, and it reproduces the file.

Several new things at once. use brings names into scope so you can write io::stdin() instead of std::io::stdin(); it is an abbreviation and nothing more. std is the standard library, available to every program without being declared as a dependency.

io::stdin() hands back a handle to the program’s input, and .lock() takes exclusive use of it, which makes reading faster because nothing has to be coordinated with other threads on each call. [0u8; 1] is an array of one u8, an unsigned 8-bit integer, initialized to zero. That array is the buffer the byte gets read into.

input.read(&mut byte) fills the buffer and reports how many bytes it managed to get: one until the input runs out and zero after. The &mut is a mutable borrow: the read function is being lent the buffer so it can write into it, and it does not take it over. That distinction is the subject of Chapter 5 and you can read it here as “pass this so it can be modified.”

The return value is not a number but a Result, either Ok(n) or an Err carrying a description of what went wrong. There is no way to use the number without acknowledging that the read might have failed, because the number is inside the Result and you have to open it to get at it. while let Ok(1) = input.read(...) opens it and tests it in one move: keep looping as long as the result is an Ok containing exactly 1. A read error or an end of input ends the loop. Chapter 8 does error handling properly.

.unwrap() on the write is the short, rude way of saying “this cannot fail, and if it does, crash”. It is fine in a ten-line program and a smell in a real one.

Counting

This is wc, more or less: lines, words, and bytes.

// Count lines, words, and bytes on standard input.
use std::io::{self, Read};

fn main() {
    let mut text = String::new();
    io::stdin().read_to_string(&mut text).expect("not valid UTF-8");

    let mut lines = 0;
    let mut words = 0;
    let mut in_word = false;

    for c in text.chars() {
        if c == '\n' {
            lines += 1;
        }
        if c == ' ' || c == '\n' || c == '\t' {
            in_word = false;
        } else if !in_word {
            in_word = true;
            words += 1;
        }
    }

    println!("{lines} {words} {}", text.len());
}

On a two-line file it agrees with the system’s wc:

$ cargo run -q -p ch01 --bin count < in.txt
2 6 28
$ wc < in.txt
 2  6 28

String::new() makes an empty, growable string, and read_to_string appends everything on the input to it. .expect("...") is .unwrap() with a message attached, and it is used here because the read really can fail: a String in Rust is guaranteed to hold valid UTF-8, so handing it bytes that are not UTF-8 has to be refused. This guarantee is unusual and it matters. In C a string is whatever bytes you put in it and you find out about the encoding later, at the worst moment.

text.chars() walks the characters, and text.len() counts the bytes. Those differ the instant anything non-English shows up, and Rust makes you say which one you meant. The word-counting logic is the state machine from K&R: you are in a word or you are not, and a transition from “not” to “in” is a new word.

Arrays

Counting occurrences of each digit, plus white space and everything else:

// Count digits, white space, and everything else.
use std::io::{self, Read};

fn main() {
    let mut text = String::new();
    io::stdin().read_to_string(&mut text).unwrap();

    let mut digits = [0u32; 10];
    let mut white = 0;
    let mut other = 0;

    for c in text.chars() {
        match c {
            '0'..='9' => digits[c as usize - '0' as usize] += 1,
            ' ' | '\t' | '\n' => white += 1,
            _ => other += 1,
        }
    }

    print!("digits =");
    for n in digits {
        print!(" {n}");
    }
    println!(", white space = {white}, other = {other}");
}
$ printf 'abc 123 4\t56\n' | cargo run -q -p ch01 --bin histogram
digits = 0 1 1 1 1 1 1 0 0 0, white space = 4, other = 3

[0u32; 10] is ten unsigned 32-bit integers, all zero. An array’s length is part of its type and fixed when you compile; [0u32; 10] and [0u32; 11] are different types. For a list that grows you want a Vec, which Chapter 7 covers. Indexing is digits[i], counting from zero, and an index past the end stops the program rather than reading whatever is next in memory.

match is the other new thing, and it is the control structure that Rust leans on hardest. It compares a value against a series of patterns and runs the arm that fits. '0'..='9' is a range pattern, ' ' | '\t' | '\n' is an alternation, and _ matches anything and is the catch-all. A match must cover every possible case, and the compiler checks that it does. Chapter 3 goes further and Chapter 6 shows why this is the feature the language is built around.

The subtraction c as usize - '0' as usize converts a character to its position in the digit range, the same trick as C’s c - '0'. usize is the integer type used for indexing, big enough to address memory on your machine.

Exercise 1-7. Write a program that prints its input one word per line.

Exercise 1-8. Count blanks, tabs, and newlines separately.

Exercise 1-9. Copy input to output, replacing each run of one or more blanks with a single blank.

1.6 Functions

A function to raise an integer to a power:

// Raise base to the n-th power; n >= 0.
fn power(base: i64, n: u32) -> i64 {
    let mut p = 1;
    for _ in 0..n {
        p *= base;
    }
    p
}

fn main() {
    for i in 0..10 {
        println!("{i} {} {}", power(2, i), power(-3, i));
    }
}
0 1 1
1 2 -3
2 4 9
3 8 -27
...
9 512 -19683

Parameters must have their types written out, and so must the return type, after the arrow. Inside the body, inference takes over again: p is an i64 because it is multiplied by one.

The last line of the function is p, with no semicolon and no return. In Rust a block’s value is the value of its last expression, and that is what the function returns. Adding a semicolon turns the expression into a statement whose value is nothing, and the function stops compiling because it promised an i64. This catches people early. return p; also works and is what you write when you need to leave from the middle.

The loop variable is named _, the way to say that you do not intend to use it. Name it i instead and the compiler warns about an unused variable. It does that sort of thing constantly, and you should let it.

Note that n is a u32, an unsigned integer, so “negative exponent” is not a case this function has to handle or document. The type says it. Choosing types so that bad inputs cannot be spelled is a habit worth building early.

Exercise 1-10. Write power using recursion.

1.7 Arguments

Arguments are passed by value, as in C: the function gets its own copy and assigning to it does not affect the caller. For numbers that is the end of the story.

For anything larger it is not, and this is where Rust stops resembling the languages you know. Consider a function that takes a String. There are three things you might mean, and Rust makes you pick.

fn consume(s: String) { ... }      // takes it over; caller loses it
fn inspect(s: &String) { ... }     // borrows it; caller keeps it
fn modify(s: &mut String) { ... }  // borrows it and may change it

The first moves the string into the function. The caller cannot use it afterwards, and the compiler enforces that, by name, with the line number where the move happened. The second and third borrow it, and the caller gets it back when the function returns.

You have already used the third form: read_to_string(&mut text) lends the string to the reader so it can append to it. Chapter 5 is entirely about this and there is nothing to memorize until then.

1.8 Strings

A last program for this chapter: print the longest line of the input.

// Print the longest input line.
use std::io::{self, BufRead};

fn main() {
    let mut longest = String::new();

    for line in io::stdin().lock().lines() {
        let line = line.expect("read error");
        if line.len() > longest.len() {
            longest = line;
        }
    }

    if !longest.is_empty() {
        println!("{} bytes: {longest}", longest.len());
    }
}
$ cargo run -q -p ch01 --bin longest < in.txt
14 bytes: this is a test

.lines() produces the lines of the input with the terminating newline already removed, each one wrapped in a Result because reading can fail. The line

let line = line.expect("read error");

declares a new variable with the same name as the old one, holding the unwrapped String. That is shadowing, and Rust encourages it: the wrapped value has no further use, so reusing the name keeps you from having to invent line_ok. The old binding is gone from that point on.

longest = line moves the string. No copy of the text is made, no allocation happens, and the old longest is freed at that moment. The program holds one line at a time and never asks how long a line might be, which in C is a fixed buffer and a decision you get wrong.

K&R’s version of this program takes three functions and a #define MAXLINE 1000. That is not a fair comparison, because C in 1978 had no growable string in its library and Rust has one in every program. The comparison to draw is different: both programs are precise about where the storage lives, and only one of them makes you do the bookkeeping by hand.

Exercise 1-11. Print all lines longer than 80 characters.

Exercise 1-12. Print each input line with trailing blanks and tabs removed, and skip lines that are empty afterwards.

Exercise 1-13. Print a histogram of the lengths of words in the input, one line per length, using * for the bars.

1.9 What You Have Seen

Enough Rust to write small programs: let and let mut, the numeric types and the refusal to mix them silently, while and for over ranges, match with its insistence on covering every case, functions whose last expression is the return value, and the beginning of the ownership story in the difference between taking a String and borrowing one.

What you have not seen is why any of it is arranged this way. The next four chapters answer that, and Chapter 5 answers the largest part of it.

2. Types, Operators, and Expressions

Variables and constants are the material a program works on; declarations say what they are; operators say what to do with them. This chapter covers the basic types and the rules for combining them, and ends with the one structural idea that separates Rust from C: almost everything is an expression.

2.1 Names and Shadowing

Names are letters, digits, and underscores, and cannot start with a digit. Case matters. Rust is firm about style and will warn you into line: variables and functions in snake_case, types in CamelCase, constants in SCREAMING_CASE.

A name can be declared again in the same scope. This is called shadowing:

#![allow(unused)]
fn main() {
let input = "  42 ";
let input = input.trim();
let input: i32 = input.parse().unwrap();
}

Three variables, one name, three types. The earlier ones still exist but can no longer be reached. This is idiomatic Rust and it is not the same as assignment: each let makes a new variable, so the value can change type and none of them has to be mut. Use it when a value is being refined toward its final form, and not to hold two unrelated things.

2.2 Integers

Rust has signed integers i8, i16, i32, i64, i128 and the unsigned u8, u16, u32, u64, u128, where the number is the width in bits. It also has isize and usize, which are as wide as a memory address on the machine you compile for, 64 bits on anything you are likely to use. usize is the type of an index and of a length, so it turns up whether or not you go looking for it.

When you do not say, Rust picks i32. It is a good default: fast everywhere, big enough for counting things.

Literals may be decimal, hexadecimal, octal, or binary, may contain underscores anywhere for legibility, and may carry the type as a suffix.

#![allow(unused)]
fn main() {
let a = 1_000_000;
let b = 0xff;
let c = 0b1010;
let d = 0o77;
let e = 42u8;
let f = 42_i64;
}

Choosing a width is a real decision and Rust will not make it for you. Use i32 unless you have a reason, u8 for bytes, usize for indices, u64 or i64 when the values might be large. Use the unsigned types to express that a quantity cannot be negative, as the power function in Chapter 1 did.

Overflow

What happens when a value will not fit is a place where Rust makes a choice C does not. In C, signed overflow is undefined behavior and the compiler is permitted to assume it never happens. In Rust the behavior is specified, and which behavior you get depends on how you compiled:

fn main() {
    let mut n: u8 = 250;
    for _ in 0..10 {
        n += 1;
        println!("{n}");
    }
}

Built with cargo run, this prints up to 255 and then stops the program:

255

thread 'main' panicked at ch02/src/bin/overflow.rs:4:9:
attempt to add with overflow

Built with cargo run --release, it wraps around to 0 and keeps going. The checks cost time, so they are on during development and off in optimized builds. That asymmetry is deliberate and it has a consequence: an overflow bug that only shows up under real workloads will crash in testing and silently produce garbage in production, which is still better than C but is not a guarantee.

When overflow is a case you actually have to handle, say so, and the behavior no longer depends on the build:

i32::MAX.checked_add(1)      // None
i32::MAX.wrapping_add(1)     // -2147483648
i32::MAX.saturating_add(1)   // 2147483647
i32::MAX.overflowing_add(1)  // (-2147483648, true)

Every arithmetic operator has these four forms. checked_ hands back an Option that is None on overflow, wrapping_ does modular arithmetic, saturating_ clamps at the limit, and overflowing_ returns the wrapped value together with a flag. Every integer type also carries MIN and MAX.

Division

Integer division truncates toward zero and the remainder takes the sign of the left operand, so -7 / 2 is -3 and -7 % 2 is -1. Division by zero is a panic, not undefined behavior and not infinity.

2.3 Floating Point

f64 is the default and f32 exists when you need half the space. Literals with a decimal point or an exponent are floats: 3.0, 1e10, 2.5f32.

The usual floating-point facts apply, since this is IEEE 754 like everywhere else. 0.1 + 0.2 is 0.30000000000000004. f64::NAN is not equal to itself. Rust makes that last one visible in the type system rather than leaving it as folklore: floats implement PartialOrd but not Ord, so a Vec<f64> has no .sort(), only .sort_by(|a, b| a.partial_cmp(b).unwrap()). When the sorting method you expected is missing, this is why.

Methods live on the type, so it is (2.0f64).sqrt(), x.abs(), x.floor(), x.powi(3), x.powf(0.5), x.is_nan(). There is no separate math library to import.

2.4 Booleans and Characters

bool is true or false and nothing else converts to it. There is no truthy or falsy: if x where x is an integer does not compile. Write if x != 0.

A char is a Unicode scalar value and occupies four bytes, so 'a', 'é', '字', and '🦀' are each one char. This is not C’s char. For a single byte you want u8, and b'A' is the byte literal for 65.

Because a char is a scalar value and a String is UTF-8 bytes, the two disagree about length in a way you have to be aware of: 'é'.len_utf8() is 2. Chapter 7 works through what that means for strings, which is the only place it really bites.

2.5 Conversions

Rust never converts number types implicitly. Not integer to float, not i32 to i64, not even when the conversion could not possibly lose anything. Three mechanisms cover the ground.

as converts between any two numeric types and never fails, which means it can lose information without saying so. 300u32 as u8 is 44, taking the low eight bits. -1i32 as u8 is 255, reinterpreting the bits. 3.9f64 as i32 is 3, truncating toward zero, and 1e10f64 as i32 is 2147483647, saturating at the maximum rather than producing nonsense. Use as when the truncation is what you meant, most often when indexing or working with bytes.

from and into convert where the conversion cannot lose anything, and are therefore the ones to reach for by default:

#![allow(unused)]
fn main() {
let count: i32 = 7;
let wide = i64::from(count);
let exact = f64::from(count);
let owned = String::from("hello");
}

try_into converts where it might fail, and hands back a Result:

#![allow(unused)]
fn main() {
let big: u64 = 200;
let small: u8 = big.try_into().expect("does not fit in a byte");
}

Text converts through parse, which also hands back a Result because text is not to be trusted:

#![allow(unused)]
fn main() {
let n: i32 = "42".parse().unwrap();
let n = "42".parse::<i32>().unwrap();
}

Both lines do the same thing. The second uses the turbofish, ::<T>, to say the type at the call rather than in the declaration. You will see it whenever there is no other place for the type to come from.

2.6 Tuples and the Unit

A tuple groups a fixed number of values of different types.

#![allow(unused)]
fn main() {
let point = (3, 4.5, 'x');
println!("{}", point.0);
let (a, b, c) = point;
}

Fields are reached by position with a dot, and a tuple can be taken apart by pattern in a let. Tuples are how a function returns two things without a struct being invented for the purpose, worth doing once or twice and not habitually; by the third field you want a struct and Chapter 6 has one.

The empty tuple () is called the unit. It is the type of an expression that has no value, and it is what a function returns when its signature has no arrow. It is a real type and it costs nothing at runtime.

2.7 Constants and Statics

const names a value that is computed at compile time and substituted wherever it is used:

#![allow(unused)]
fn main() {
const MAX_LINE: usize = 1000;
}

The type is required. A const has no address of its own and no lifetime concerns, which makes it the right choice nearly always.

static names a value that lives at a fixed address for the whole run of the program. Read-only statics are ordinary:

#![allow(unused)]
fn main() {
static GREETING: &str = "hello";
}

A mutable static is shared state that any thread could touch, so reading or writing one requires unsafe, Rust declining to promise something it cannot check. If you find yourself wanting one, you want a different design; the standard library has the pieces, and this book does not need them.

2.8 Operators

Arithmetic is + - * / %, with no exponent operator: use x.pow(n) for integers and x.powf(y) for floats. Comparison is == != < <= > >= and works only between two values of the same type. Logic is && || !, and && and || short-circuit. Bitwise is & | ^ ! << >>, where ! is the complement rather than C’s ~, and doubles as boolean not; which one it means follows from the type of its operand.

Assignment is =, and every arithmetic and bitwise operator has a compound form such as += and <<=. There is no ++ or --, and assignment is not an expression: a = b = c does not compile and neither does if (x = 1), which removes a whole family of C bugs by construction.

Precedence, from tightest to loosest:

LevelOperators
1paths ::, method calls, field access, indexing []
2?
3unary - ! * & &mut
4as
5* / %
6+ -
7<< >>
8&
9^
10|
11== != < > <= >=
12&&
13||
14.. ..=
15= and the compound assignments

Two entries are not where a C programmer expects. as binds tighter than multiplication, so 2 + 3 as f64 converts only the 3. And the bitwise operators bind tighter than the comparisons, so x & y == 0b1000 means (x & y) == 0b1000, which is what you wanted and what C refuses to give you. Comparisons do not chain: a < b < c is a type error rather than a surprise.

2.9 Expressions and Statements

Rust distinguishes expressions, which have a value, from statements, which do not. The distinction is easy to state and worth taking seriously, because more things are expressions than you are used to.

A block in braces is an expression whose value is its last expression, with no semicolon on that last line:

#![allow(unused)]
fn main() {
let x = {
    let a = 3;
    let b = 4;
    a * a + b * b
};
}

if is an expression:

#![allow(unused)]
fn main() {
let n = 7;
let size = if n < 10 { "small" } else { "large" };
}

Both arms must have the same type, and there is no ternary operator because this is the ternary operator. match is an expression in the same way, and Chapter 3 uses it that way constantly.

A semicolon turns an expression into a statement and throws its value away. That is the rule behind the most common beginner error in Rust: a function whose final line ends in a semicolon returns () rather than the value you computed, and the compiler tells you so while pointing at the semicolon.

Certain expressions never produce a value at all, because control leaves before they finish: return, break, continue, and panic!. Their type is written ! and it fits anywhere, so this works:

#![allow(unused)]
fn main() {
fn show(text: &str) {
    let n: i32 = match text.parse() {
        Ok(v) => v,
        Err(_) => return,
    };
    println!("{n}");
}
}

The second arm has no i32 to offer, and does not need one.

Exercise 2-1. Print the minimum and maximum of every integer type using MIN and MAX, and work out from the output which types are signed.

Exercise 2-2. Write a function that takes an i64 and returns its absolute value without using .abs(). What should it do for i64::MIN? Look at what .abs() itself does, and at .checked_abs().

Exercise 2-3. Write a function bits(n: u32) -> u32 that counts the one-bits in a number using & and >>. Compare your answer with n.count_ones().

Exercise 2-4. 0.1 + 0.2 == 0.3 is false. Write a function that compares two floats to within a tolerance, and find a pair of values for which your function gives an answer you would call wrong.

3. Control Flow

Rust’s control structures will look familiar for about five minutes. The shapes are the ones you know, with parentheses dropped and braces required. What is new is match, which does the work that switch, if-chains, destructuring, and type tests do in other languages, and does it with the compiler checking that you have not forgotten a case.

3.1 if and else

#![allow(unused)]
fn main() {
let n = 7;
if n % 2 == 0 {
    println!("even");
} else if n % 3 == 0 {
    println!("divisible by three");
} else {
    println!("neither");
}
}

The condition must be a bool. No parentheses, no truthiness, and the braces are required even for a single statement, which retires the dangling-else problem and C’s misindented-second-line bug.

Because if is an expression it can produce a value, and both arms must then agree on the type:

#![allow(unused)]
fn main() {
let n = 7;
let parity = if n % 2 == 0 { "even" } else { "odd" };
}

Leave off the else and the value is (), so an if without an else can only be used as a statement. That is the rule; the compiler’s phrasing of it, when you trip, is that the arms have incompatible types.

3.2 match

match compares a value against patterns in order and takes the first that fits. FizzBuzz, written against the pair of remainders:

fn main() {
    for n in 1..=15 {
        let line = match (n % 3, n % 5) {
            (0, 0) => "FizzBuzz".to_string(),
            (0, _) => "Fizz".to_string(),
            (_, 0) => "Buzz".to_string(),
            _ => n.to_string(),
        };
        println!("{line}");
    }
}

The patterns here are tuples, _ matches anything, and the arms are expressions separated by commas. Every arm has to produce the same type, which is why the numbers are converted with .to_string(): three arms offer text, so the fourth must too.

The property that makes match more than a switch is exhaustiveness. Leave a case out and the program does not compile:

error[E0004]: non-exhaustive patterns: `i32::MIN..=0_i32` and `3_i32..=i32::MAX` not covered
 --> non_exhaustive.rs:3:22
  |
3 |     let name = match n {
  |                      ^ patterns `i32::MIN..=0_i32` and `3_i32..=i32::MAX` not covered

For an integer that is mostly a nuisance and you write _ at the end. For the types in Chapter 6, where a value is one of a fixed set of alternatives, it is the feature the language is built around: add a new alternative and the compiler lists every place in the program that now has a hole in it.

Patterns

A pattern can be a literal, a range, an alternation, a binding, or a structure taken apart. All of these are arms:

0 => ...,                      // a literal
1..=9 => ...,                  // a range, both ends included
'a' | 'e' | 'i' | 'o' | 'u' => // alternation
n => ...,                      // binds anything to the name n
_ => ...,                      // matches anything, binds nothing
(x, 0) => ...,                 // a tuple, second element zero
[first, .., last] => ...,      // a slice, first and last elements

An arm can carry a guard, an if that runs after the pattern fits:

n if n < 0 => "negative",

and a binding can be attached to a pattern with @ so you can both test a value and keep it:

n @ 1..=9 => println!("single digit {n}"),

Here is all of it at once. The program reads words and says what each looks like:

// Read words from standard input and say what each one looks like.
use std::io::{self, Read};

fn classify(word: &str) -> String {
    match word {
        "" => "empty".to_string(),
        "yes" | "no" => format!("an answer: {word}"),
        w if w.parse::<i64>().is_ok() => format!("the number {w}"),
        w if w.parse::<f64>().is_ok() => format!("the float {w}"),
        w if w.chars().all(|c| c.is_ascii_alphabetic()) => match w.len() {
            1 => "a single letter".to_string(),
            2..=4 => "a short word".to_string(),
            _ => "a long word".to_string(),
        },
        w => format!("something else ({} chars)", w.chars().count()),
    }
}

fn main() {
    let mut text = String::new();
    io::stdin().read_to_string(&mut text).unwrap();

    for word in text.split_whitespace() {
        println!("{word}: {}", classify(word));
    }
}
$ printf 'no 42 3.5 x cat elephant hello! 7up\n' | cargo run -q -p ch03 --bin classify
no: an answer: no
42: the number 42
3.5: the float 3.5
x: a single letter
cat: a short word
elephant: a long word
hello!: something else (6 chars)
7up: something else (3 chars)

Order matters: "no" reaches the second arm before the guards get a chance, and 7up fails the numeric parse and the all-alphabetic test and falls to the end. Arms can nest, as the word-length match does. format! builds a String the way println! builds a line of output.

w if ... binds the word and then tests it. The closure |c| c.is_ascii_alphabetic() is Chapter 4’s business; read it for now as a small unnamed function passed to all.

3.3 Loops

Three loops, and they divide up cleanly. while runs while a condition holds. for walks a sequence. loop runs until something breaks out of it, and is the one C spells for (;;).

// How many steps does each starting value take to reach 1?
fn steps(mut n: u64) -> u32 {
    let mut count = 0;
    while n != 1 {
        n = if n % 2 == 0 { n / 2 } else { 3 * n + 1 };
        count += 1;
    }
    count
}

fn main() {
    let mut worst = (0, 0);
    for start in 1..1000 {
        let s = steps(start);
        if s > worst.1 {
            worst = (start, s);
        }
    }
    println!("longest under 1000: {} takes {} steps", worst.0, worst.1);
}
longest under 1000: 871 takes 178 steps

Note mut n: u64 in the parameter list. The function gets its own copy of the number and is free to modify it, and the caller’s value is untouched. That is the same call-by-value C has, said out loud.

break leaves the innermost loop and continue starts its next round. A loop can hand a value out through its break, the tidy way to write a search that has to run at least once:

#![allow(unused)]
fn main() {
let mut n = 1;
let first_big_square = loop {
    if n * n > 1000 {
        break n * n;
    }
    n += 1;
};
println!("{first_big_square}");
}

When loops nest and you need to leave more than one, label them:

#![allow(unused)]
fn main() {
let grid = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
let target = 5;
let mut found = None;

'rows: for (r, row) in grid.iter().enumerate() {
    for (c, value) in row.iter().enumerate() {
        if *value == target {
            found = Some((r, c));
            break 'rows;
        }
    }
}
println!("{found:?}");
}

A label is a name with a leading apostrophe, and break 'rows leaves the loop wearing it. This replaces the goto that C reaches for here; Rust has no goto.

.enumerate() pairs each item with its position, and .iter() walks a collection by reference. Chapter 7 covers both properly.

3.4 if let, let else, and while let

Much of the time a match has one arm you care about and a _ => {} for everything else. if let is the short form:

#![allow(unused)]
fn main() {
let maybe_port: Option<u16> = "8080".parse().ok();

if let Some(port) = maybe_port {
    println!("listening on {port}");
} else {
    println!("no port given");
}
}

let ... else is for the other common shape, where a failure means this function has nothing more to do:

#![allow(unused)]
fn main() {
fn show(text: &str) {
    let Ok(n) = text.parse::<i32>() else {
        println!("{text} is not a number");
        return;
    };
    println!("{}", n * 2);
}
}

The pattern binds for the rest of the enclosing block, and the else branch has to leave: return, break, continue, or a panic. It is how you keep the happy path flat instead of drifting rightwards with every check.

while let loops as long as the pattern keeps fitting, and is how you drain something:

#![allow(unused)]
fn main() {
let mut stack = vec![1, 2, 3];

while let Some(top) = stack.pop() {
    println!("{top}");
}
}

pop hands back Some(value) until the vector is empty and then None, which ends the loop. Chapter 1’s byte-copying program used the same construction on a Result.

3.5 What Is Missing

There is no goto, no do ... while (write loop with the test at the bottom), no fallthrough between match arms, no ternary operator, and no exceptions. The last of those is the significant one: a Rust function that can fail says so in its return type, and Chapter 8 is about what you do with that.

Exercise 3-1. Write the FizzBuzz program again using if/else if. Which version would you rather modify when a third divisor is added?

Exercise 3-2. Print the Collatz sequence itself for a starting value read from the command line, rather than its length. std::env::args().nth(1) gives you the first argument as an Option<String>.

Exercise 3-3. Write a function that takes a char and returns its category as a &'static str: vowel, consonant, digit, white space, or other. Use one match.

Exercise 3-4. The classify program tests is_ok() on a parse and throws the result away, then parses again. Rewrite it so each word is parsed at most once.

Exercise 3-5. Write a loop that finds the smallest positive integer n for which n * n has at least six digits, using loop and break with a value.

4. Functions

Functions in Rust hold few surprises. Closures hold a few more, because a closure has to say what it does with the variables it captures, and that turns out to be the same question ownership answers everywhere else. This chapter takes both, and leaves modules and testing to Chapter 10.

4.1 Declarations

#![allow(unused)]
fn main() {
fn gcd(mut a: u64, mut b: u64) -> u64 {
    while b != 0 {
        let t = b;
        b = a % b;
        a = t;
    }
    a
}
}

Parameter types and the return type are required; nothing about a signature is inferred. Inside the body, inference does the work.

Functions may be declared in any order and a function may call one declared later in the file. There are no forward declarations and no header files.

The value of the function is the value of its final expression, written without a semicolon. return exists for leaving early:

#![allow(unused)]
fn main() {
fn first_word(text: &str) -> &str {
    for (i, c) in text.char_indices() {
        if c == ' ' {
            return &text[..i];
        }
    }
    text
}
}

A function with no arrow returns (), and return; in such a function is legal. A function that never returns at all, such as one that always panics or loops forever, can be declared -> !.

A parameter may be declared mut, as gcd’s two are. The function then has a modifiable copy of the argument, and the caller sees nothing. mut on a parameter is part of the body, not part of the interface; callers never write it and it does not appear in documentation.

4.2 Arguments

Arguments are passed by value. For a number or a bool or a char, “by value” means a copy, and the cost is a register.

For anything that owns memory, passing by value hands over the ownership. The caller no longer has the value. That is often what you want, and when it is not, you pass a reference with & and the caller keeps it:

#![allow(unused)]
fn main() {
fn count_vowels(text: &str) -> usize {
    text.chars().filter(|c| "aeiou".contains(*c)).count()
}
}

Taking &str rather than String means this function works on a String, on a string literal, and on a slice of either, and it borrows rather than consumes. That is the default shape for a function that only reads its argument, and Chapter 5 explains what the ampersand costs and guarantees.

4.3 Recursion

Recursion works as you would expect, and Rust does not promise tail-call elimination, so a recursive function deep enough to matter will exhaust the stack. For the depths that come up in ordinary work it is fine:

#![allow(unused)]
fn main() {
fn fib(n: u32) -> u64 {
    if n < 2 {
        u64::from(n)
    } else {
        fib(n - 1) + fib(n - 2)
    }
}
}

This is the slow definition, doing exponential work to recompute the same values. The loop version is three lines and instantaneous. Prefer iteration where it is natural; keep recursion for data that is recursive, such as the trees in Chapter 6.

4.4 Closures

A closure is a function written inline that can capture variables from around it:

#![allow(unused)]
fn main() {
let factor = 3;
let scale = |x: i32| x * factor;
println!("{} {}", scale(5), scale(7));
}

The parameters go between vertical bars and the body follows. Types are usually inferred and usually left out: |x| x * factor is what you would write. A body of more than one expression takes braces.

What makes closures worth a section is capture. A closure that only reads factor borrows it. A closure that assigns to a captured variable borrows it mutably, and must itself be declared mut:

#![allow(unused)]
fn main() {
let mut calls = 0;
let mut counted = |x: i32| {
    calls += 1;
    x * 2
};
counted(1);
counted(2);
println!("called {calls} times");
}

A closure marked move takes ownership of everything it captures, needed when it has to outlive the scope that made it, such as a closure handed to another thread or returned from a function:

#![allow(unused)]
fn main() {
let name = String::from("world");
let greet = move || println!("hello, {name}");
greet();
}

After that move, name belongs to the closure and the surrounding code cannot use it. The compiler decides borrow or borrow-mutably on its own; move is the one part you say out loud.

4.5 Passing Functions Around

A function that takes a closure declares what it needs to do with it. Three traits describe the choices, and they nest: Fn can be called repeatedly and only reads what it captured, FnMut can be called repeatedly and may modify what it captured, and FnOnce can be called once because calling it consumes something. Take the least demanding one that works, nearly always Fn.

Here is bisection, which takes any function from f64 to f64:

// Find a root of f between lo and hi, where f(lo) and f(hi) have opposite signs.
fn bisect(f: impl Fn(f64) -> f64, mut lo: f64, mut hi: f64) -> f64 {
    for _ in 0..60 {
        let mid = (lo + hi) / 2.0;
        if f(lo) * f(mid) <= 0.0 {
            hi = mid;
        } else {
            lo = mid;
        }
    }
    (lo + hi) / 2.0
}

fn main() {
    let root = bisect(|x| x * x - 2.0, 0.0, 2.0);
    println!("sqrt(2) = {root:.10}");

    let target = 10.0;
    let root = bisect(move |x| x * x * x - target, 0.0, 10.0);
    println!("cbrt(10) = {root:.10}");
}
sqrt(2) = 1.4142135624
cbrt(10) = 2.1544346900

impl Fn(f64) -> f64 in a parameter position means “some type that can be called this way”, decided at compile time. The closure is compiled into the function directly, so this costs nothing over writing the arithmetic inline. The same notation in the return position means “some type I am not naming”, and is how you return a closure:

#![allow(unused)]
fn main() {
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
    move |x| x + n
}
}

The move is required. Without it the closure would borrow n, and n is gone the moment the function returns.

A plain function can be passed wherever a closure is expected, and fn(i32) -> i32 is the type of a function that captures nothing:

fn map_all(values: &[i32], f: fn(i32) -> i32) -> Vec<i32> {
    let mut out = Vec::new();
    for &v in values {
        out.push(f(v));
    }
    out
}

fn double(n: i32) -> i32 {
    n * 2
}

fn main() {
    let values = [1, 2, 3, 4];
    println!("{:?}", map_all(&values, double));
    println!("{:?}", map_all(&values, |n| n * n));
}
[2, 4, 6, 8]
[1, 4, 9, 16]

Note for &v in values: the loop over a slice produces references, and the & in the pattern takes the value out of the reference. Writing for v in values and then f(*v) says the same thing. Both forms are everywhere in real code.

Closures are how the standard library is meant to be used. Sorting takes one:

#![allow(unused)]
fn main() {
let mut words = vec!["pear", "fig", "banana", "kiwi"];
words.sort_by_key(|w| w.len());
println!("{words:?}");
}
["fig", "pear", "kiwi", "banana"]

sort_by_key sorts by whatever the closure computes. The sort is stable, so pear and kiwi, both of length four, keep the order they came in with. sort_by takes a two-argument comparison instead, for when there is no single key. {:?} is the debugging format, which prints structures rather than requiring them to be presentable; Chapter 9 explains where it comes from.

Exercise 4-1. Write power from Chapter 1 as a recursive function that squares its way down, so that power(2, 10) performs four multiplications rather than ten.

Exercise 4-2. Write apply_twice(f, x) that returns f(f(x)). What is its signature?

Exercise 4-3. Use bisect to find where cos(x) - x is zero, and check the answer against a calculator.

Exercise 4-4. bisect calls f(lo) on every round even though lo usually has not changed. Rewrite it to call f once per iteration.

Exercise 4-5. Write a function that takes a slice of words and returns the longest, and decide what it should do when two are tied. What type should it return when the slice is empty?

5. Ownership

Every value in a running program occupies memory, and something has to decide when that memory is released. C leaves the decision to you and punishes mistakes with corruption. Python, JavaScript, and Lux hand the decision to a garbage collector, which costs a runtime, some memory, and a pause at an unpredictable moment. Rust decides at compile time, from rules about who owns what, and emits the frees itself. No collector, no malloc and free in your code, and the whole apparatus disappears when the program is built.

That is what you are buying. The price is this chapter. Read it slowly; it is the only part of the book that asks you to.

5.1 Where Values Live

Two places. The stack holds a frame for each function call, and everything in a frame has a size known at compile time and disappears when the function returns. The heap holds anything whose size is decided while the program runs or whose lifetime outlasts the call that made it.

A String is both at once:

#![allow(unused)]
fn main() {
let s = String::from("hello");
}

The variable s is three machine words on the stack: a pointer to the characters, the number of bytes in use, and the number of bytes allocated. The characters themselves are on the heap. You can see the shape:

i32      4
&str     16
String   24
Vec<i32> 24
&String  8

Eight bytes per word on a 64-bit machine, so String and Vec are three words and a plain reference is one. &str is two, a pointer and a length, and is therefore not the same kind of thing as a C pointer at all.

The distinction that matters is not stack against heap but owning against borrowing. A String owns its bytes and will free them. A &str refers to bytes that somebody else owns, and frees nothing.

5.2 Move

Assignment of an owning value transfers ownership:

let s1 = String::from("hello");
let s2 = s1;
println!("{s1} {s2}");

The three words are copied into s2, and s1 is marked dead. The heap bytes are not copied and are not touched. Then the print, which uses s1, does not compile:

error[E0382]: borrow of moved value: `s1`
 --> moved_value.rs:4:16
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |     println!("{s1} {s2}");
  |                ^^ value borrowed here after move
  |
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let s2 = s1.clone();
  |                ++++++++

This is the error you will meet most often. The message names where the value came from, where it went, and where you tried to use it after.

The reason for the rule is the free. Exactly one variable is responsible for the heap bytes, and that variable frees them when it goes out of scope. If both s1 and s2 were live, both would free, and a double free is one of the classic ways a C program becomes a security advisory.

Passing to a function is the same transfer:

fn consume(s: String) { }

let s = String::from("hello");
consume(s);
println!("{s}");        // error: value moved into consume

Returning transfers it back out. A function that takes a value and hands it back modified is an ordinary shape in Rust and costs nothing at runtime, since only the three words move.

5.3 Copy

Small values behave the way you expect instead:

#![allow(unused)]
fn main() {
let a = 5;
let b = a;
println!("{a} {b}");
}

Types that are nothing but bits, with no heap and no cleanup, are Copy: assignment duplicates them and both remain usable. All the integer and floating types, bool, char, shared references, and tuples and arrays made entirely of Copy types. String, Vec, and anything owning a resource are not, and neither is &mut T, because two usable mutable references to the same thing is the situation these rules exist to prevent.

When you want a duplicate of something that is not Copy, ask:

#![allow(unused)]
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{s1} {s2}");
}

clone really does copy the heap bytes, and it is visible in the source because it is the expensive one. Rust’s position is that a deep copy is fine as long as nobody is surprised by it.

5.4 Drop

When a value goes out of scope, its memory is released. If the type has cleanup to do, the Drop implementation runs first:

struct Noisy(&'static str);

impl Drop for Noisy {
    fn drop(&mut self) {
        println!("dropping {}", self.0);
    }
}

fn main() {
    let _a = Noisy("a");
    {
        let _b = Noisy("b");
        println!("inner block ends");
    }
    let _c = Noisy("c");
    println!("main ends");
}
inner block ends
dropping b
main ends
dropping c
dropping a

Values are dropped at the end of their scope, in reverse order of declaration. A moved-from value is not dropped, because it is no longer the owner.

This is how files close, locks release, and sockets shut down: not because you remembered, but because the owner went out of scope. There is no finally and no with statement in Rust, and no need for either.

5.5 Borrowing

Handing ownership to every function that wants a look at a value would be tedious. A reference lends it instead:

// Borrowing: the caller keeps the text and gets a slice of it back.
fn first_word(text: &str) -> &str {
    match text.find(' ') {
        Some(i) => &text[..i],
        None => text,
    }
}

fn shout(text: &mut String) {
    text.make_ascii_uppercase();
    text.push('!');
}

fn main() {
    let mut line = String::from("hello there world");
    println!("first word: {}", first_word(&line));

    shout(&mut line);
    println!("{line}");
    println!("still owned here, {} bytes", line.len());
}
first word: hello
HELLO THERE WORLD!
still owned here, 18 bytes

&line is a shared borrow: the function may read and must not modify. &mut line is a mutable borrow: the function may modify, and the variable being lent has to be mut itself. In both cases main still owns the string and is still responsible for freeing it.

Note that no * appears in the bodies. Rust dereferences automatically when you call a method or reach a field, so text.len() works whether text is a String or a &String. You need * to read through a reference to something simple, as in *count += 1 or comparing *value == target.

The rules

At any given moment, for any given value, you may have either any number of shared references or exactly one mutable reference, and never both. A reference may never outlive the thing it points at.

That is the system. Everything the compiler tells you about borrows follows from those two sentences.

The payoff is worth naming, because it goes well beyond memory safety. If you hold a &mut, nothing else in the program can be looking at that value, so nobody can observe it half-modified. If you hold a &, nothing can be changing it underneath you. The iterator-invalidation bug, where a collection is modified while something walks it, becomes a compile error:

error[E0502]: cannot borrow `names` as mutable because it is also borrowed as immutable
 --> borrow_conflict.rs:4:5
  |
3 |     let first = &names[0];
  |                  ----- immutable borrow occurs here
4 |     names.push(String::from("grace"));
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
5 |     println!("{first}");
  |                ----- immutable borrow later used here

That program is not merely inelegant. push may reallocate, and first would then point into freed memory. In C++ it compiles and mostly works and occasionally does not. Here it is rejected with the three relevant lines underlined.

The same rules catch data races when threads are involved, so Rust’s concurrency story is short: the compiler already refuses shared mutable access, and threads add no new rule.

Borrows end at their last use

A borrow lasts from where it is taken to where it is last used, not to the end of the block:

fn main() {
    let mut v = vec![1, 2, 3];

    let first = &v[0];
    println!("first is {first}");
    // The borrow above is over: its last use was the line before.

    v.push(4);
    println!("{v:?}");
}

This compiles. Move the println! that uses first below the push and it does not. When an error talks about a borrow “later used here”, that line is what is keeping the borrow alive, and moving it is often the fix.

5.6 Slices

A slice is a borrowed view of part of a sequence: a pointer and a length, no allocation, no copy.

#![allow(unused)]
fn main() {
let text = String::from("hello there world");
let hello = &text[0..5];
let world = &text[12..];
println!("{hello} {world}");

let numbers = [1, 2, 3, 4, 5];
let middle = &numbers[1..4];
println!("{middle:?} sums to {}", middle.iter().sum::<i32>());
}

&str is a slice of a string and &[T] is a slice of an array or vector. Since a slice borrows, the compiler will not let the underlying value be modified or dropped while the slice is alive. That is what makes returning a slice from a function safe.

Write function parameters as slices rather than owned collections wherever you only need to read: &str instead of &String, &[i32] instead of &Vec<i32>. The slice version accepts more kinds of argument and asks for less.

String slice indices are byte positions, and slicing through the middle of a multi-byte character stops the program. Chapter 7 has the details.

5.7 Lifetimes

A reference is only good while the thing it refers to is alive. The compiler tracks that with lifetimes, and most of the time it works them out silently. Occasionally it cannot, and then it asks:

error[E0106]: missing lifetime specifier
 --> missing_lifetime.rs:1:33
  |
1 | fn longest(a: &str, b: &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 `a` or `b`
help: consider introducing a named lifetime parameter
  |
1 | fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

The function returns a reference into one of its arguments, and the compiler needs to know which so it can check the caller. 'a is a lifetime parameter, a name for “some region of the program”, and the annotated signature says: the result is good for as long as both arguments are. Take the compiler’s suggestion and the function compiles:

#![allow(unused)]
fn main() {
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() > b.len() { a } else { b }
}
}

Lifetime annotations never change what the program does. They are a claim the compiler checks, not an instruction it follows.

You will write few of them, because three elision rules cover ordinary signatures. Each elided input lifetime gets its own. If there is exactly one input lifetime, it is given to every output. And if one of the parameters is &self, its lifetime is given to every output. That last rule is why methods almost never need annotations, and the second is why first_word above did not.

The other common message is a value not living long enough, which means a reference has outlasted its referent. Trying to return a reference to a local:

error[E0106]: missing lifetime specifier
 --> dangling.rs:1:14
  |
1 | fn make() -> &String {
  |              ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: instead, you are more likely to want to return an owned value

The help is right. Return the String, not a reference to it. This is the dangling pointer, and in Rust it is a compile error with a suggestion attached.

One lifetime has a name of its own. 'static means “lives for the whole program”, and every string literal has it, because literals are baked into the executable. That is why a function can return &'static str without borrowing from anything.

5.8 Getting Unstuck

Some practical advice for the first weeks, when the borrow checker feels like an adversary rather than a colleague.

When you cannot see the fix, clone(). A clone is a real cost but it is a correct program, and you can remove it later once you understand why it was needed. Everyone does this and nobody should feel clever about avoiding it on the first draft.

When a function needs two pieces of a structure at once and the compiler objects to the second borrow, take both at the start, or take the whole structure mutably once, or split the work into a function that returns what you need. Fighting for a second borrow usually means the design wants restructuring.

When a loop is modifying the collection it is walking, collect the changes into a separate vector and apply them after the loop. This is better code in any language; Rust just insists on it.

When ownership genuinely needs to be shared, the standard library has Rc for sharing within a thread and Arc across threads, and RefCell for moving the borrow check to runtime. They are the right answer for graphs and for trees with parent pointers, and the wrong answer for the first problem you reach for them on. Use them when you have understood why the plain approach cannot work.

And read the error before changing anything. Rust’s diagnostics name the variable, mark the three relevant lines, and frequently write the correction. People who find Rust painful are usually people who skim.

Exercise 5-1. Write a function that takes a String, appends an exclamation mark, and returns it. Write it again taking &mut String. When is each the better interface?

Exercise 5-2. Write fn longest_word(text: &str) -> &str. Why does this one need no lifetime annotation?

Exercise 5-3. Make the borrow checker produce E0499, two mutable borrows at once, then fix it in two different ways.

Exercise 5-4. Noisy prints when dropped. Add a function that takes a Noisy by value and does nothing with it, call it, and predict from the output where the drop happens.

Exercise 5-5. This does not compile. Explain why in one sentence, then fix it without using clone:

fn main() {
    let mut words = vec![String::from("one"), String::from("two")];
    for w in &words {
        if w.len() == 3 {
            words.push(w.to_uppercase());
        }
    }
}

6. Structs and Enums

A struct groups values that belong together. An enum names a value that is one of several alternatives. Between them they describe nearly all the data a program handles, and Rust’s version of the second one is more useful than what most languages offer, because the compiler makes you deal with every alternative.

6.1 Structs

struct Rect {
    width: f64,
    height: f64,
}

fn main() {
    let a = Rect { width: 3.0, height: 4.0 };
    println!("{}", a.width * a.height);
}

Fields are named at construction, in any order, and reached with a dot. A struct is mutable only if the variable holding it is declared mut; there is no per-field mutability.

Two shorthands come up constantly. When a variable has the same name as the field, write it once:

fn new(width: f64, height: f64) -> Rect {
    Rect { width, height }
}

And when most of a new value comes from an existing one, take the rest with ..:

let wide = Rect { width: 10.0, ..a };

There are two other shapes. A tuple struct has fields without names, reached by position, and is how you wrap a single value in a meaningful type:

struct Celsius(f64);
struct Meters(f64);

fn main() {
    let t = Celsius(21.5);
    println!("{}", t.0);
}

Those two are different types, so a function taking Meters cannot be handed a Celsius, and a whole family of unit-confusion bugs stops compiling. The wrapper costs nothing at runtime.

A unit struct has no fields at all, and exists to have behavior attached to it:

#![allow(unused)]
fn main() {
struct Logger;
}

6.2 Methods

Functions belonging to a type go in an impl block:

#[derive(Debug, Clone, Copy, PartialEq)]
struct Rect {
    width: f64,
    height: f64,
}

impl Rect {
    fn new(width: f64, height: f64) -> Rect {
        Rect { width, height }
    }

    fn square(side: f64) -> Rect {
        Rect::new(side, side)
    }

    fn area(&self) -> f64 {
        self.width * self.height
    }

    fn scale(&mut self, factor: f64) {
        self.width *= factor;
        self.height *= factor;
    }
}

fn main() {
    let a = Rect::new(3.0, 4.0);
    println!("{a:?} area {}", a.area());

    let mut c = a;
    c.scale(2.0);
    println!("scaled {c:?}, original {a:?}");
}
Rect { width: 3.0, height: 4.0 } area 12
scaled Rect { width: 6.0, height: 8.0 }, original Rect { width: 3.0, height: 4.0 }

The first parameter decides everything. &self borrows, and is what a method that only reads should take. &mut self borrows mutably, for a method that modifies. Plain self consumes the value, which is right for a conversion that should leave the original unusable. A function with no self at all is an associated function, called as Rect::new(...) rather than on a value; new is a convention rather than a keyword, and it is how Rust spells a constructor.

Methods are called with a dot and Rust adds the borrow for you: a.area() means Rect::area(&a). This automatic referencing is why methods read cleanly whether you hold a value or a reference to one.

You may write as many impl blocks for a type as you like.

6.3 Derive

The line above the struct asks the compiler to write some implementations:

#[derive(Debug, Clone, Copy, PartialEq)]

Debug gives the type the programmer-facing printed form {:?} uses, and {:#?} gives the same thing across several lines:

Rect {
    width: 3.0,
    height: 4.0,
}

Clone gives it .clone(). Copy makes assignment duplicate rather than move, allowed only when every field is Copy and suited to small arithmetic types like this one. PartialEq gives it ==. Others you will reach for are Default, Eq, Hash and PartialOrd. Chapter 9 explains what a derive actually is; for now, take it as the compiler writing the obvious code so you do not have to.

Derive Debug on everything. The cost is nothing and the alternative is being unable to print your own data while debugging.

6.4 Enums

An enum lists alternatives:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq)]
enum Direction {
    North,
    South,
    East,
    West,
}
}

A value of type Direction is one of those four and nothing else. There is no integer hiding underneath that someone can set to 9.

Enums get impl blocks like structs, and a method on an enum is usually a match:

impl Direction {
    fn parse(word: &str) -> Option<Direction> {
        match word {
            "n" | "north" => Some(Direction::North),
            "s" | "south" => Some(Direction::South),
            "e" | "east" => Some(Direction::East),
            "w" | "west" => Some(Direction::West),
            _ => None,
        }
    }

    fn opposite(self) -> Direction {
        match self {
            Direction::North => Direction::South,
            Direction::South => Direction::North,
            Direction::East => Direction::West,
            Direction::West => Direction::East,
        }
    }
}

opposite takes self by value rather than by reference, which is right here because Direction is Copy and one machine word. Add a fifth direction and the compiler will reject opposite until you extend it. That is the property worth paying for: the type system knows the set of cases, and every place that handles them is checked against the list.

Variants carrying data

A variant may carry values, and different variants may carry different things:

enum Shape {
    Circle(f64),
    Rect { width: f64, height: f64 },
    Triangle(f64, f64, f64),
}

fn area(s: &Shape) -> f64 {
    match s {
        Shape::Circle(r) => std::f64::consts::PI * r * r,
        Shape::Rect { width, height } => width * height,
        Shape::Triangle(a, b, c) => {
            let s = (a + b + c) / 2.0;
            (s * (s - a) * (s - b) * (s - c)).sqrt()
        }
    }
}

fn main() {
    let shapes = [
        Shape::Circle(1.0),
        Shape::Rect { width: 3.0, height: 4.0 },
        Shape::Triangle(3.0, 4.0, 5.0),
    ];
    for s in &shapes {
        println!("{:.4}", area(s));
    }
}

This is a tagged union, and it is the feature that people coming from C miss most when they go back. The value carries its own tag, the tag cannot disagree with the contents, and match binds the contents while it tests the tag.

The patterns take the variants apart: Shape::Circle(r) binds the radius, Shape::Rect { width, height } binds both fields by name. Because s is a reference, the bindings are references too, and the arithmetic works on them without a * because the operators know what to do with a &f64.

Note that the match arms hold the whole calculation. There is no partially constructed object to guard against and no “this field is only meaningful when the kind is Circle” comment.

6.5 Option

Rust has no null. A value that might be missing is wrapped:

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

That is the entire definition, and it is in the standard library rather than the language. Some and None are usable without a prefix in every program.

The consequence is that a missing value cannot be mistaken for a present one, because the two have different shapes and you have to open the wrapper to get at the contents. There is no equivalent of reading a field off null and finding out at runtime. Tony Hoare called the null reference his billion-dollar mistake; this is what not making it looks like.

The direct way to open an Option is match, or if let when only one case is interesting. There are also methods for the common shapes, and using them keeps code flat:

#![allow(unused)]
fn main() {
let name: Option<String> = None;

println!("{}", name.clone().unwrap_or_else(|| "anonymous".to_string()));
println!("{}", name.is_some());

let n: Option<i32> = Some(4);
println!("{:?}", n.map(|v| v * 10));
println!("{}", n.unwrap_or(0));
println!("{:?}", n.filter(|v| *v > 10));
}

unwrap_or supplies a default, unwrap_or_else supplies one lazily, map applies a function to the value if there is one, and_then does the same when the function itself returns an Option, and ok_or turns a None into an error, which Chapter 8 needs. unwrap and expect take the value and panic if it is not there; they are for cases you have already ruled out, and for the first draft.

Here is Option doing real work. A little text adventure where the exits are optional:

struct Room {
    name: String,
    north: Option<usize>,
    south: Option<usize>,
    east: Option<usize>,
    west: Option<usize>,
}

impl Room {
    fn exit(&self, d: Direction) -> Option<usize> {
        match d {
            Direction::North => self.north,
            Direction::South => self.south,
            Direction::East => self.east,
            Direction::West => self.west,
        }
    }
}

with the walking loop as a match inside a match:

match Direction::parse(word) {
    None => println!("{word}: I do not know that direction."),
    Some(d) => match world[here].exit(d) {
        Some(next) => {
            here = next;
            println!("{word}: you are in the {}.", world[here].name);
        }
        None => println!("{word}: no exit that way; try {:?}.", d.opposite()),
    },
}
north: you are in the library.
south: you are in the hall.
east: you are in the kitchen.
up: I do not know that direction.
west: you are in the hall.

Two different absences, an unknown word and a missing exit, are handled in two different places, and neither can be skipped by accident. The rooms refer to each other by index into a vector rather than by reference, which sidesteps the ownership question entirely; it is the usual way to build a graph in Rust and it is worth learning before reaching for Rc.

6.6 Result

Result is Option with a reason attached:

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

Every fallible operation in the standard library returns one, so parse and read_to_string have needed unwrapping since Chapter 1. Chapter 8 is about handling them properly.

6.7 Recursive Types

An enum that contains itself would have no fixed size, so the recursive part goes behind a Box, a pointer to the heap and one word wide:

enum Expr {
    Number(f64),
    Neg(Box<Expr>),
    Add(Box<Expr>, Box<Expr>),
    Mul(Box<Expr>, Box<Expr>),
    Div(Box<Expr>, Box<Expr>),
}

use Expr::{Add, Div, Mul, Neg, Number};

fn eval(e: &Expr) -> Option<f64> {
    match e {
        Number(n) => Some(*n),
        Neg(a) => Some(-eval(a)?),
        Add(a, b) => Some(eval(a)? + eval(b)?),
        Mul(a, b) => Some(eval(a)? * eval(b)?),
        Div(a, b) => {
            let divisor = eval(b)?;
            if divisor == 0.0 {
                None
            } else {
                Some(eval(a)? / divisor)
            }
        }
    }
}

fn show(e: &Expr) -> String {
    match e {
        Number(n) => n.to_string(),
        Neg(a) => format!("-{}", show(a)),
        Add(a, b) => format!("({} + {})", show(a), show(b)),
        Mul(a, b) => format!("({} * {})", show(a), show(b)),
        Div(a, b) => format!("({} / {})", show(a), show(b)),
    }
}

fn main() {
    // (3 + 4) * -2
    let e = Mul(
        Box::new(Add(Box::new(Number(3.0)), Box::new(Number(4.0)))),
        Box::new(Neg(Box::new(Number(2.0)))),
    );
    println!("{} = {:?}", show(&e), eval(&e));

    let bad = Div(Box::new(Number(1.0)), Box::new(Number(0.0)));
    println!("{} = {:?}", show(&bad), eval(&bad));
}
((3 + 4) * -2) = Some(-14.0)
(1 / 0) = None

Box::new(x) puts x on the heap and owns it; when the Box is dropped the contents go too. It is the simplest of Rust’s pointer types and the one to reach for when a type needs to contain itself.

The use Expr::{...} line brings the variant names into scope so the tree can be written without repeating the type name.

The question mark in eval(a)? is a preview of Chapter 8. On an Option, it means: if this is None, return None from the enclosing function right now; otherwise unwrap it and carry on. Three characters replace a match whose failure arm does nothing interesting, and the division case shows the long form alongside it for contrast.

Exercise 6-1. Add a Sub variant to Expr and let the compiler find every place that needs updating. Note how many places that is, and how it found them.

Exercise 6-2. eval returns Option<f64>, so a division by zero and an unparseable input are indistinguishable. Change it to return Result<f64, String> with a message naming the problem.

Exercise 6-3. Give Rect a Default implementation by deriving it, and find out what the default f64 is.

Exercise 6-4. The Room struct has four nearly identical fields. Replace them with a single exits: Vec<(Direction, usize)> and rewrite exit. Which version would you rather extend with “up” and “down”?

Exercise 6-5. Write an enum Token for a small calculator language, with variants for numbers, the four operators, and parentheses. Write a function that turns a &str into a Vec<Token>, returning None on anything unrecognized.

7. Collections, Strings, and Iterators

Three subjects that belong together, because in Rust they are one subject: the standard collections, the text type that is not quite a collection, and the iterator machinery that is how you are meant to work with both.

7.1 Vec

A Vec<T> is a growable array of values of one type, stored contiguously on the heap. It is the collection you will use for nearly everything.

fn main() {
    let mut v: Vec<i32> = Vec::new();
    v.push(3);
    v.push(1);
    v.push(2);

    let mut w = vec![10, 20, 30];
    w.extend_from_slice(&[40, 50]);

    println!("{v:?} {w:?}");
    println!("first {:?}, tenth {:?}", w.first(), w.get(9));
    println!("popped {:?}, now {:?}", w.pop(), w);

    v.sort();
    println!("sorted {v:?}, contains 2: {}", v.contains(&2));

    let mut n = vec![1, 2, 3, 4, 5, 6];
    n.retain(|x| x % 2 == 0);
    println!("evens {n:?}");

    let total: i32 = n.iter().sum();
    println!("total {total}, max {:?}", n.iter().max());
}
[3, 1, 2] [10, 20, 30, 40, 50]
first Some(10), tenth None
popped Some(50), now [10, 20, 30, 40]
sorted [1, 2, 3], contains 2: true
evens [2, 4, 6]
total 12, max Some(6)

vec![...] is the macro that builds one from a list, and vec![0; 100] builds a hundred zeros. The type is written once, either in the declaration or by inference from the first push.

Two ways to read an element. v[i] panics if i is out of range:

thread 'main' panicked at index_out_of_bounds.rs:3:21:
index out of bounds: the len is 3 but the index is 5

and v.get(i) hands back an Option, the one to use when the index came from outside the program. first, last, and pop are all Option for the same reason: the vector might be empty, and the type says so.

sort needs the elements to be orderable, which floats are not (Chapter 2), so a Vec<f64> uses sort_by(|a, b| a.partial_cmp(b).unwrap()). dedup removes consecutive duplicates and is usually preceded by a sort. retain keeps the elements a closure approves of. insert and remove take an index and shift everything after it, so they cost time proportional to the length; swap_remove is the fast alternative when order does not matter.

A vector grows by allocating a larger block and moving its contents, so the address of an element is not stable across a push. That is the reason for the borrow rule you met in Chapter 5, and the reason Vec::with_capacity(n) exists when you know the size in advance.

7.2 Slices Again

Passing a whole vector to a function that only reads it means passing &[T], not &Vec<T>. The slice form accepts a vector, an array, or part of either:

fn mean(values: &[f64]) -> Option<f64> {
    if values.is_empty() {
        return None;
    }
    Some(values.iter().sum::<f64>() / values.len() as f64)
}

fn main() {
    let v = vec![1.0, 2.0, 3.0, 4.0];
    println!("{:?}", mean(&v));
    println!("{:?}", mean(&v[..2]));
    println!("{:?}", mean(&[]));
}

Useful slice methods beyond the obvious: windows(n) gives every overlapping run of n elements, chunks(n) gives non-overlapping ones, split_at(i) divides in two, and concat and join put a slice of slices back together.

7.3 Arrays

An array [T; N] has its length in its type, lives wherever it is declared rather than on the heap, and cannot grow. Use one for a fixed-size buffer or a small lookup table, as Chapter 1’s digit counter did. Everything else wants a Vec.

7.4 HashMap

use std::collections::{BTreeMap, HashMap, HashSet};

fn main() {
    let text = "the quick brown fox jumps over the lazy dog the fox";

    let mut counts: HashMap<&str, u32> = HashMap::new();
    for word in text.split_whitespace() {
        *counts.entry(word).or_insert(0) += 1;
    }
    println!("the appears {} times", counts["the"]);
    println!("cat appears {:?} times", counts.get("cat"));

    let mut pairs: Vec<(&str, u32)> = counts.into_iter().collect();
    pairs.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
    println!("{:?}", &pairs[..3]);

    let ordered: BTreeMap<&str, usize> =
        text.split_whitespace().map(|w| (w, w.len())).collect();
    println!("{ordered:?}");

    let unique: HashSet<&str> = text.split_whitespace().collect();
    println!("{} words, {} distinct", text.split_whitespace().count(), unique.len());
}
the appears 3 times
cat appears None times
[("the", 3), ("fox", 2), ("brown", 1)]
{"brown": 5, "dog": 3, "fox": 3, "jumps": 5, "lazy": 4, "over": 4, "quick": 5, "the": 3}
11 words, 8 distinct

The line to learn is this one:

*counts.entry(word).or_insert(0) += 1;

entry names a place in the map, whether or not anything is there yet. or_insert(0) puts a zero there if it was empty and hands back a mutable reference to the value either way, and the * writes through it. Counting things is the most common map operation there is, and this is one line with one lookup rather than a contains-then-get-then-insert dance.

map[key] panics on a missing key; map.get(key) returns an Option. Use the second unless you are certain.

Iteration order of a HashMap is unspecified and varies between runs, on purpose. When you need order, either collect into a Vec and sort, as the example does, or use a BTreeMap, which keeps its keys sorted and iterates in that order at the cost of slower lookups. HashSet and BTreeSet are the same pair without values.

collect is doing quiet work in three places above: it builds a Vec of pairs from a map, a BTreeMap from pairs, and a HashSet from words. What it builds comes from the type you asked for, which is why those declarations carry explicit types.

Keys must be hashable and comparable, which for your own types means deriving Hash, Eq, and PartialEq. Floats are not eligible, for the reason given in Chapter 2.

7.5 Strings

String owns growable UTF-8 text. &str is a borrowed view of UTF-8 text, usually of part of a String or of a literal baked into the executable. Take &str in function parameters, return String when you have made something new.

fn main() {
    let mut s = String::from("hello");
    s.push_str(", world");
    s.push('!');

    let name = "Ada";
    let greeting = format!("{s} My name is {name}.");
    println!("{greeting}");

    let text = "  Grüße, Welt  ";
    println!("[{}]", text.trim());
    println!("bytes {} chars {}", text.trim().len(), text.trim().chars().count());
    println!("upper {}", text.trim().to_uppercase());
    println!("{:?}", text.trim().find('W'));
    println!("{:?}", text.trim().split(", ").collect::<Vec<&str>>());
    println!("{}", text.trim().replace("Welt", "World"));

    for (i, c) in "Grüße".char_indices() {
        print!("{i}:{c} ");
    }
    println!();
}
hello, world! My name is Ada.
[Grüße, Welt]
bytes 13 chars 11
upper GRÜSSE, WELT
Some(9)
["Grüße", "Welt"]
Grüße, World
0:G 1:r 2:ü 4:ß 6:e 

Read that output carefully, because it contains everything awkward about text. Eleven characters occupy thirteen bytes. The character positions from char_indices are 0, 1, 2, 4, 6, with gaps where a character took two bytes. find reports byte 9 for the W, not character 9. And uppercasing Grüße produces GRÜSSE, which is one character longer than it started, because the uppercase of ß is two letters.

So: len() is bytes, chars().count() is characters and costs a walk of the whole string, and indices are byte offsets everywhere. &s[0..3] is legal and takes bytes 0 to 3, but only if that lands on character boundaries:

thread 'main' panicked at char_boundary.rs:3:22:
end byte index 3 is not a char boundary; it is inside 'ü' (bytes 2..4 of string)

This looks like Rust being difficult. It is Rust making you notice a problem every language has and most hide until your program meets a name with an accent in it. Slice with indices that came from the string itself, from find or char_indices, and nothing can go wrong.

One further layer exists that the standard library does not address: what a person calls a character can be several Unicode scalars, as with a flag emoji or an e followed by a combining accent. If you need that, the unicode-segmentation crate gives you grapheme clusters. Most programs do not.

The methods you will want: trim, trim_start, trim_end, split, split_whitespace, splitn, lines, starts_with, ends_with, contains, find, replace, to_lowercase, to_uppercase, parse, repeat, and chars().rev() for backwards. Building text is format! for a one-off, push_str in a loop, and String::with_capacity when the size is known.

+ concatenates but takes ownership of its left side and borrows its right: a + &b. It is a wart. Use format!.

7.6 Iterators

An iterator produces values one at a time. Everything in this chapter has one, and most of what you want to do with a collection is a chain of iterator steps ending in something that consumes the chain.

fn main() {
    let words = ["apple", "fig", "banana", "kiwi", "cherry"];

    let shouted: Vec<String> = words
        .iter()
        .filter(|w| w.len() > 3)
        .map(|w| w.to_uppercase())
        .collect();
    println!("{shouted:?}");

    let total_length: usize = words.iter().map(|w| w.len()).sum();
    println!("{total_length}");

    let first_long = words.iter().find(|w| w.len() > 5);
    println!("{first_long:?}");

    let all_lower = words.iter().all(|w| w.chars().all(|c| c.is_lowercase()));
    println!("{all_lower}");

    for (i, w) in words.iter().enumerate().take(2) {
        println!("{i} {w}");
    }

    let pairs: Vec<(usize, &str)> = (1..).zip(words.iter().copied()).take(3).collect();
    println!("{pairs:?}");
}
["APPLE", "BANANA", "KIWI", "CHERRY"]
24
Some("banana")
true
0 apple
1 fig
[(1, "apple"), (2, "fig"), (3, "banana")]

Adapters build a new iterator and do no work: map, filter, take, skip, rev, zip, enumerate, chain, flat_map, step_by, peekable, filter_map. Consumers run the chain: collect, sum, count, fold, min, max, any, all, find, position, last, for_each.

Nothing happens until a consumer asks. That is why (1..), an endless range, appears in the example without hanging the program: take(3) only ever asks for three. It is also why a chain of five adapters is compiled into one loop with no intermediate collections, and runs as fast as the loop you would have written.

Three ways to start an iterator from a collection, and the difference is ownership. .iter() produces &T and leaves the collection alone. .iter_mut() produces &mut T so you can modify in place. .into_iter() consumes the collection and produces T, for when you are done with the original. A for loop over &v is .iter(), over &mut v is .iter_mut(), and over v is .into_iter() and ends the vector’s life.

collect deserves a second mention because it is the most versatile function in the library. It builds whatever the target type says: a Vec, a String, a HashMap from an iterator of pairs, a HashSet, and, in a trick Chapter 8 comes back to, a Result<Vec<T>, E> from an iterator of Results, which is how you parse a list of things and fail on the first bad one.

Here it is doing real work. The five longest distinct words on the input:

// The five longest distinct words on the input, longest first.
use std::collections::HashSet;
use std::io::{self, Read};

fn main() {
    let mut text = String::new();
    io::stdin().read_to_string(&mut text).unwrap();

    let seen: HashSet<String> = text
        .split_whitespace()
        .map(|w| w.trim_matches(|c: char| !c.is_alphanumeric()).to_lowercase())
        .filter(|w| !w.is_empty())
        .collect();

    let mut words: Vec<String> = seen.into_iter().collect();
    words.sort_by(|a, b| b.chars().count().cmp(&a.chars().count()).then(a.cmp(b)));

    for w in words.iter().take(5) {
        println!("{:3} {w}", w.chars().count());
    }
}
$ printf 'The quick brown fox jumps over the lazy dog, extraordinarily.\nUnbelievable! the fox.\n' | cargo run -q -p ch07 --bin longwords
 15 extraordinarily
 12 unbelievable
  5 brown
  5 jumps
  5 quick

The punctuation is stripped by trim_matches with a closure, the case is folded so The and the are one word, the HashSet does the duplicate removal, and the sort puts longest first with ties broken alphabetically by then. Each step is one line and none of them needs a temporary vector.

Exercise 7-1. Write a function that takes &[i32] and returns the second largest value as an Option<i32>. Decide what to do about duplicates and say so in a comment.

Exercise 7-2. Count the frequency of each character in the input and print the ten most common, highest first.

Exercise 7-3. Write fn is_palindrome(s: &str) -> bool that ignores case, spaces, and punctuation. Test it with something containing an accented letter.

Exercise 7-4. Rewrite longwords to keep only the five longest as it goes, rather than collecting and sorting everything. Which version is clearer, and at what input size does that stop mattering?

Exercise 7-5. words.sort_by(...) sorts by length descending, then alphabetically. Write the same comparison with sort_by_key and a tuple key. Why does the descending part make that awkward?

8. Errors

Rust has no exceptions. A function that can fail says so in its return type, and the caller cannot ignore it without writing something that looks like ignoring it. That one decision shapes how Rust programs are put together, and it is worth understanding what it buys: you can read a signature and know what can go wrong, and you can read a call site and see where failure is handled.

Failures come in two kinds and Rust treats them differently. A bug is unrecoverable: an index out of range, an invariant broken, a case the programmer swore could not happen. The program stops. Everything else is expected: a file that is not there, input that is not a number, a network that is down. Those are values, and the type is Result.

8.1 Panic

panic! stops the current thread, prints a message and the line, and unwinds, running every destructor on the way out.

panic!("the world is inside out: {state}");

Most panics are not written by hand. They come from unwrap on an empty Option, from indexing past the end of a vector, from integer overflow in a debug build, from division by zero, or from an assert! that did not hold.

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

Setting that environment variable gives you the call stack, usually all you need.

Panic when a condition means the program’s assumptions are broken, which includes most of what you would assert in C. Do not panic because a file was missing or a user typed something odd; that is ordinary life and it deserves a Result.

unwrap and expect are panics in disguise. They are fine in a first draft, in a test, and in a place where you have just established that the value is there. Prefer expect with a sentence saying what you were assuming, because that sentence is what you will read at three in the morning.

8.2 Result

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

Handling one with match is always available:

use std::fs;

fn main() {
    match fs::read_to_string("numbers.txt") {
        Ok(text) => println!("{} bytes", text.len()),
        Err(e) => println!("could not read it: {e}"),
    }
}

and there are the same shorthand methods Option has. unwrap_or and unwrap_or_else supply a fallback. map transforms the success value and map_err transforms the error. ok() throws the error away and gives you an Option. is_ok() and is_err() ask without consuming.

Ignoring a Result entirely gets you a warning, because the return value is marked #[must_use]. When you genuinely do not care, say so with let _ = ..., a deliberate act rather than an oversight.

8.3 The Question Mark

Most of the time a function cannot handle the failure it just received and should hand it to its caller. That is what ? does:

let text = fs::read_to_string(path)?;

If the result is Ok, the value comes out and the program continues. If it is Err, the function returns that error immediately. It is the match you were going to write, in one character.

Two conditions. The enclosing function must return a Result itself, and the error type must convert into the function’s error type. The conversion is automatic where a From implementation exists, and that is what makes the next program work: read_to_string fails with an io::Error and parse fails with a ParseIntError, and both convert into a boxed error.

// Sum the numbers in a file, one per line.
use std::error::Error;
use std::fs;

fn sum_file(path: &str) -> Result<i64, Box<dyn Error>> {
    let text = fs::read_to_string(path)?;
    let mut total = 0;
    for line in text.lines() {
        let line = line.trim();
        if !line.is_empty() {
            total += line.parse::<i64>()?;
        }
    }
    Ok(total)
}

fn main() -> Result<(), Box<dyn Error>> {
    let path = std::env::args().nth(1).unwrap_or_else(|| "numbers.txt".to_string());
    println!("{} sums to {}", path, sum_file(&path)?);
    Ok(())
}
$ cargo run -q -p ch08 --bin read_file numbers.txt
numbers.txt sums to 42

$ cargo run -q -p ch08 --bin read_file /nope.txt
Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }
$ echo $?
1

Box<dyn Error> means “some error, whatever kind”, and it is the right choice for a program’s own internals, where you mostly want to report and stop. It is the wrong choice for a library, whose callers may want to tell one failure from another; for that, define the error type, as the next section does.

Note that main itself returns a Result. When it returns an Err, the runtime prints the error’s debug form and exits with status 1. That is a lot of correct behavior for one line of signature, and it is why main is written this way in most real programs.

? works on Option too, inside a function that returns Option, where it means “give up and return None”. Chapter 6’s expression evaluator used it that way. It does not convert between the two: use ok_or to turn a None into an error, and .ok() to turn an error into a None.

8.4 Your Own Error Type

An enum, one variant per thing that can go wrong, each carrying what a reader would need to fix it:

#[derive(Debug)]
enum ConfigError {
    MissingEquals { line: usize },
    EmptyKey { line: usize },
    BadNumber { line: usize, text: String },
}

To be an error type it needs to print itself. Display is the message a user sees, Debug is the one a programmer sees and is derived, and implementing std::error::Error is what lets it convert into a Box<dyn Error> and join the general machinery:

impl fmt::Display for ConfigError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            ConfigError::MissingEquals { line } => write!(f, "line {line}: no '=' found"),
            ConfigError::EmptyKey { line } => write!(f, "line {line}: empty key"),
            ConfigError::BadNumber { line, text } => {
                write!(f, "line {line}: {text:?} is not a number")
            }
        }
    }
}

impl std::error::Error for ConfigError {}

Now the parser. Note where each error is produced: ok_or turns the missing separator into one, map_err converts the standard library’s parse failure into one, and both are followed by ?.

fn parse(text: &str) -> Result<Vec<(String, i64)>, ConfigError> {
    let mut out = Vec::new();

    for (i, raw) in text.lines().enumerate() {
        let line = i + 1;
        let raw = raw.trim();
        if raw.is_empty() || raw.starts_with('#') {
            continue;
        }

        let (key, value) = raw.split_once('=').ok_or(ConfigError::MissingEquals { line })?;
        let key = key.trim();
        if key.is_empty() {
            return Err(ConfigError::EmptyKey { line });
        }

        let value = value.trim();
        let n = value.parse::<i64>().map_err(|_| ConfigError::BadNumber {
            line,
            text: value.to_string(),
        })?;

        out.push((key.to_string(), n));
    }

    Ok(out)
}
Ok([("width", 80), ("height", 24), ("depth", 3)])
error: line 1: no '=' found
error: line 1: empty key
error: line 1: "eighty" is not a number

The messages name the line number, the difference between an error report and an annoyance. Errors are for the person reading them; the type system only makes sure they arrive.

Writing Display by hand for every error type gets tiresome, and real projects use the thiserror crate to derive it, or anyhow to skip defining the type at all in programs where Box<dyn Error> would do. Chapter 10 covers adding dependencies. Learn the manual version first: both crates are generating what you just wrote.

8.5 Collecting Results

An iterator of Results can be collected two ways, and the choice says what should happen to the bad ones:

fn main() {
    let good = ["1", "2", "3"];
    let bad = ["1", "two", "3"];

    let parsed: Result<Vec<i64>, _> = good.iter().map(|s| s.parse::<i64>()).collect();
    println!("{parsed:?}");

    let parsed: Result<Vec<i64>, _> = bad.iter().map(|s| s.parse::<i64>()).collect();
    println!("{parsed:?}");

    let kept: Vec<i64> = bad.iter().filter_map(|s| s.parse().ok()).collect();
    println!("{kept:?}");
}
Ok([1, 2, 3])
Err(ParseIntError { kind: InvalidDigit })
[1, 3]

Collecting into Result<Vec<_>, _> stops at the first failure and hands back that error. Collecting through filter_map with .ok() keeps what parsed and silently drops the rest. Both are one line, and the line says which policy you chose, more than most languages let you express.

8.6 A Working Rule

Return Result from anything that touches the outside world or interprets input someone else wrote. Use ? to carry failures up to a level that can do something about them, which is usually main or the top of a subsystem. Define an error enum when callers might branch on the kind of failure, and use Box<dyn Error> when they will not. Panic only for bugs. Never write a function that returns Option when the caller will want to know why it was empty.

Exercise 8-1. Give sum_file a real error type with variants for the read failing and for a line not parsing, and include the line number and its text in the second.

Exercise 8-2. parse stops at the first bad line. Write a version that returns Result<Vec<(String, i64)>, Vec<ConfigError>>, reporting every bad line at once. Which behavior would you want from a compiler, and which from a configuration file?

Exercise 8-3. What happens if a key appears twice? Decide on a policy, write it into the code, and add the error variant if you chose to reject it.

Exercise 8-4. Write a function that reads a file of numbers and returns their mean, and give it a sensible answer for an empty file. What type does that answer have?

Exercise 8-5. Replace one ? in sum_file with the match it stands for. Count the lines you added.

9. Traits and Generics

A generic function works for many types. A trait says what a type can do. The two are one mechanism: a generic function states which traits its types must implement, and the compiler checks that before generating any code. Nearly everything the standard library offers is expressed this way, so you cannot read real Rust without reading traits.

9.1 Generic Functions

Write the type as a parameter and use it in the signature:

fn largest<T: PartialOrd>(items: &[T]) -> Option<&T> {
    let mut it = items.iter();
    let mut best = it.next()?;
    for item in it {
        if item > best {
            best = item;
        }
    }
    Some(best)
}

fn main() {
    println!("{:?}", largest(&[3, 9, 2, 7]));
    println!("{:?}", largest(&["fig", "apple", "pear"]));
    println!("{:?}", largest(&[1.5, 0.5]));
    println!("{:?}", largest::<i32>(&[]));
}
Some(9)
Some("pear")
Some(1.5)
None

<T: PartialOrd> introduces a type parameter and a bound: T can be any type that can be compared with <. Without the bound the function does not compile, because nothing is known about T and > might not exist for it. This is the difference from C++ templates, where the same function compiles and then fails inside the body when instantiated with the wrong type. In Rust the signature is a contract checked once.

The compiler produces a separate copy of the function for each type actually used, so there is no dispatch at runtime and no penalty against a hand-written version. Note also that the empty case needed a type, largest::<i32>(&[]), because nothing in the call says what the elements would have been.

Structs can be generic too, and the impl block repeats the parameter:

struct Pair<T> {
    first: T,
    second: T,
}

impl<T: PartialOrd + Copy> Pair<T> {
    fn larger(&self) -> T {
        if self.first > self.second { self.first } else { self.second }
    }
}

Multiple bounds are joined with +. When the list gets long, move it to a where clause, the same thing with more room:

fn report<T>(items: &[T]) -> String
where
    T: fmt::Display + Clone,
{
    ...
}

9.2 Defining a Trait

A trait is a set of method signatures, and may supply default bodies:

trait Shape {
    fn area(&self) -> f64;

    // A default method: implementors may override it or take this one.
    fn describe(&self) -> String {
        format!("a shape of area {:.2}", self.area())
    }
}

Implementing it for a type is a separate block:

impl Shape for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }

    fn describe(&self) -> String {
        format!("a circle of radius {}", self.radius)
    }
}

impl Shape for Square {
    fn area(&self) -> f64 {
        self.side * self.side
    }
}

Circle overrides describe and Square takes the default:

a circle of radius 1
a shape of area 4.00

The trait and the type are declared separately, which means you can implement your own trait for a type from the standard library, or implement a standard trait for your own type. What you cannot do is implement someone else’s trait for someone else’s type, since two crates could then disagree; that is the orphan rule, and the way around it is to wrap the foreign type in a tuple struct of your own.

9.3 Static and Dynamic Dispatch

A function that takes any shape can be written two ways.

fn print_area(s: &impl Shape) {
    println!("{:.4}", s.area());
}

&impl Shape is shorthand for a generic parameter with a bound, and it is resolved at compile time: one copy of the function per type, with the call inlined. Fast, and the type is fixed at each call site.

When you need a collection holding different types at once, you need the other kind:

let shapes: Vec<Box<dyn Shape>> = vec![
    Box::new(Circle { radius: 2.0 }),
    Box::new(Square { side: 3.0 }),
];
let total: f64 = shapes.iter().map(|s| s.area()).sum();

dyn Shape is a trait object: a value of some type that implements Shape, with the identity forgotten. It is a pair of pointers, one to the data and one to a table of the type’s implementations, and the call goes through that table. This is what other languages call an interface, and it costs an indirection.

Choose static dispatch by default and reach for dyn when the set of types is open or is decided at runtime. When the set is small and closed, an enum, as in Chapter 6, is often better than either: no allocation, no indirection, and the compiler checks that every match covers every case.

9.4 Traits You Already Use

Most of the standard library is traits, and recognizing the important ones is most of what “learning the library” means.

TraitWhat it gives you
Debug{:?}, derived, for programmers
Display{}, written by hand, for users
Clone, Copy.clone(), and copy-on-assign
PartialEq, Eq==
PartialOrd, Ord<, sorting, .max()
Hashuse as a HashMap key
DefaultType::default()
From, Intoconversions, and ?
Iteratorfor loops and every adapter
Dropcleanup at end of scope

derive writes the mechanical ones for you. The interesting ones are written by hand.

Display is one of those, because only you know how your type should read:

impl fmt::Display for Celsius {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{:.1}°C", self.0)
    }
}

write! is println! aimed at a destination rather than at standard output, and returns the fmt::Result the method owes. Implementing Display also gets you .to_string() at no extra cost, because there is a blanket implementation in the library that says so.

From is the other one worth writing. Implement it and you get .into() in the other direction for free, and ? learns how to convert your error types:

use std::fmt;

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
struct Celsius(f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
struct Fahrenheit(f64);

impl From<Celsius> for Fahrenheit {
    fn from(c: Celsius) -> Fahrenheit {
        Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
    }
}

impl fmt::Display for Celsius {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{:.1}°C", self.0)
    }
}

impl fmt::Display for Fahrenheit {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{:.1}°F", self.0)
    }
}

fn main() {
    let boiling = Celsius(100.0);
    let f: Fahrenheit = boiling.into();
    println!("{boiling} is {f}");
    println!("{}", Fahrenheit::from(Celsius(-40.0)));
    println!("{}", Celsius(21.0) > Celsius(18.0));
}
100.0°C is 212.0°F
-40.0°F
true

Deriving PartialOrd on a tuple struct compares the field, so Celsius values order themselves, and a Vec<Celsius> sorts. This is the temperature program from Chapter 1 with the mistakes made impossible: a Fahrenheit cannot be passed where a Celsius belongs, and the conversion has one definition.

9.5 Implementing Iterator

Implement one method and the entire adapter library applies to your type:

struct Fib {
    a: u64,
    b: u64,
}

impl Iterator for Fib {
    type Item = u64;

    fn next(&mut self) -> Option<u64> {
        let out = self.a;
        self.a = self.b;
        self.b = out + self.b;
        Some(out)
    }
}

fn fib() -> Fib {
    Fib { a: 0, b: 1 }
}

fn main() {
    let first: Vec<u64> = fib().take(10).collect();
    println!("{first:?}");

    let even_sum: u64 = fib().take_while(|n| *n < 1000).filter(|n| n % 2 == 0).sum();
    println!("{even_sum}");

    for (i, n) in fib().skip(50).enumerate().take(3) {
        println!("{} {n}", i + 50);
    }
}
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
798
50 12586269025
51 20365011074
52 32951280099

type Item = u64 is an associated type: a type the implementation supplies rather than the caller. next returns None to finish, and this iterator never does, which is why every use of it is bounded by take or take_while. The fiftieth Fibonacci number arrives without the first fifty being stored anywhere.

This is the pattern to imitate whenever you write something that produces a sequence. Write next, get map, filter, zip, sum, collect, and the for loop, all of them compiled down to the loop you would have written by hand.

9.6 What Traits Are Not

Traits are not classes. There is no inheritance of data, no base type, and no overriding of one implementation by another. A trait may require another trait, written trait Ord: PartialOrd, which means an implementor must supply both, but that is a requirement rather than inheritance.

Nor are traits duck typing. Everything is checked when the code is compiled, and a missing implementation is an error at the call, naming the trait and the type. If you are coming from Python, the mental move is from “it will work if the method happens to exist” to “say what you need and the compiler guarantees it”.

Exercise 9-1. Add a perimeter method to Shape with no default, and find out what the compiler says about the existing implementations.

Exercise 9-2. Write a trait Summary with a default summarize that uses a required title method. Implement it for two types, overriding the default in one.

Exercise 9-3. Implement Iterator for a type that yields the lines of a string in reverse order.

Exercise 9-4. Write fn print_all<T: fmt::Display>(items: &[T]). Then rewrite it with impl Trait, and again with a where clause. All three compile to the same thing; which reads best?

Exercise 9-5. Chapter 6 modeled shapes as an enum and this chapter modeled them as a trait. Write down the change each version makes easy and the change each makes hard, then decide which you would use for a drawing program and which for a geometry library.

10. Building a Program

Everything so far has been the language. This chapter is the rest of the job: how a project is laid out, how code is split into modules, how other people’s code gets in, how tests are written, and how the whole thing is documented. It ends with a complete program that uses all of it.

10.1 Projects

cargo new name makes a program and cargo new --lib name makes a library. The difference is one file: src/main.rs with a main function, or src/lib.rs without one. A project can have both, the usual arrangement for anything that deserves tests: the work lives in the library and main.rs is a thin wrapper around it.

Cargo.toml is the manifest. It names the package, sets the edition, and lists dependencies. The edition is Rust’s way of making breaking changes without splitting the language: editions interoperate, and a crate says which one it is written in.

Additional programs go in src/bin/, one file each. That is how the examples for this book are arranged. cargo run --bin name picks one.

Cargo builds into target/, which you do not commit. Cargo.lock records the version of every dependency actually used; commit it for a program, and for a library do whatever the project’s convention is.

10.2 Modules

A module is a namespace with a privacy boundary. Declaring one in a file:

mod geometry {
    pub struct Point {
        pub x: f64,
        pub y: f64,
    }

    pub fn distance(a: &Point, b: &Point) -> f64 {
        ((a.x - b.x).powi(2) + (a.y - b.y).powi(2)).sqrt()
    }
}

use geometry::{distance, Point};

fn main() {
    let a = Point { x: 0.0, y: 0.0 };
    let b = Point { x: 3.0, y: 4.0 };
    println!("{}", distance(&a, &b));
}

Everything is private to its module unless marked pub, and privacy is per item: a public struct may have private fields, which is how you keep an invariant. A module can see its parent’s private items, but not a sibling’s.

Modules usually live in their own files. mod counter; in lib.rs tells the compiler to look for src/counter.rs, or src/counter/mod.rs when the module has children of its own. There is no include and no header; the mod line is the declaration.

Paths start from the crate root with crate::, from the current module with self::, or from the parent with super::. A use line shortens a path and nothing more, and pub use re-exports, which lets a library present a tidy surface regardless of how its files are arranged.

10.3 Dependencies

cargo add rand adds a line to Cargo.toml and fetches the crate from crates.io on the next build. Versions follow semantic versioning, and "0.8" means any compatible 0.8 release. cargo update moves within those bounds; changing the major version is an edit you make deliberately.

A few crates are near-universal and worth knowing by name: serde for turning data into JSON and back, clap for command-line parsing, rand for random numbers, regex, chrono for dates, reqwest for HTTP, tokio for async, anyhow and thiserror for the error handling of Chapter 8. The standard library is deliberately small and does not include any of these.

Be a little careful. Every dependency is code you are shipping and a supply-chain risk you are taking, and the ease of cargo add makes it easy to end up with three hundred of them. cargo tree shows what you have actually pulled in.

10.4 Tests

A test is a function marked #[test] that panics when something is wrong. assert!, assert_eq!, and assert_ne! do the panicking, and assert_eq! prints both values when it fails.

Unit tests live in the file they test, in a module marked #[cfg(test)] so they are compiled only for cargo test:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn counts_are_case_insensitive() {
        let counts = count("The the THE cat");
        assert_eq!(counts["the"], 3);
        assert_eq!(counts["cat"], 1);
        assert_eq!(counts.len(), 2);
    }
}

use super::* brings in the module being tested, including its private items, and reaching them is the point of putting tests inside the file.

Integration tests live in tests/, one file per test binary, and can only use the public interface, which makes them a check on that interface as much as on the behavior.

cargo test runs everything, in parallel, capturing output from passing tests. cargo test name runs the tests whose names contain that string, and cargo test -- --nocapture lets the passing ones print.

A test that should panic is marked #[should_panic], and a test may return Result so ? can be used inside it.

10.5 Documentation

/// before an item documents it, in Markdown, and //! at the top of a file documents the file’s module. cargo doc --open builds the HTML and shows it, including the documentation for every dependency at the version you are actually using. It is the fastest way to answer a question about a library.

Code blocks in documentation are compiled and run by cargo test:

/// The `n` most frequent words, most frequent first, ties broken alphabetically.
///
/// ```
/// let top = wf::counter::top(&wf::counter::count("a b a c a b"), 2);
/// assert_eq!(top[0], ("a".to_string(), 3));
/// assert_eq!(top[1], ("b".to_string(), 2));
/// ```

Documentation that is wrong fails the build. This is the best feature in the toolchain and it is available in no other language’s standard workflow.

10.6 The Program

wf reads text from files or standard input and prints the most frequent words. It is small, but it has the shape of a real program: a library with tests, a binary that parses arguments and reports errors, and documentation that is checked.

src/counter.rs, the work:

//! Counting words in text.

use std::collections::HashMap;

/// A word is a run of letters, digits, and apostrophes, folded to lower case.
pub fn words(text: &str) -> impl Iterator<Item = String> + '_ {
    text.split(|c: char| !(c.is_alphanumeric() || c == '\''))
        .filter(|w| !w.is_empty())
        .map(|w| w.to_lowercase())
}

/// Count how often each word appears.
pub fn count(text: &str) -> HashMap<String, usize> {
    let mut counts = HashMap::new();
    for word in words(text) {
        *counts.entry(word).or_insert(0) += 1;
    }
    counts
}

/// The `n` most frequent words, most frequent first, ties broken alphabetically.
pub fn top(counts: &HashMap<String, usize>, n: usize) -> Vec<(String, usize)> {
    let mut pairs: Vec<(String, usize)> =
        counts.iter().map(|(w, c)| (w.clone(), *c)).collect();
    pairs.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
    pairs.truncate(n);
    pairs
}

The return type of words says a good deal. impl Iterator<Item = String> + '_ means “some iterator of strings, borrowing from the argument”. The '_ is the lifetime of text, and it is required because the iterator holds a reference into the string it is splitting. Nothing is collected, so a large file is processed as it is walked.

src/main.rs, the program:

//! wf - print the most frequent words in a file or on standard input.
//!
//!     wf [-n COUNT] [FILE ...]

use std::error::Error;
use std::io::{self, Read};
use std::process;

use wf::counter;

struct Options {
    count: usize,
    files: Vec<String>,
}

fn parse_args(args: Vec<String>) -> Result<Options, String> {
    let mut options = Options { count: 10, files: Vec::new() };
    let mut rest = args.into_iter();

    while let Some(arg) = rest.next() {
        match arg.as_str() {
            "-n" => {
                let value = rest.next().ok_or("-n needs a number")?;
                options.count = value.parse().map_err(|_| format!("{value:?} is not a number"))?;
            }
            "-h" | "--help" => return Err("usage: wf [-n COUNT] [FILE ...]".to_string()),
            _ => options.files.push(arg),
        }
    }

    Ok(options)
}

fn read_input(files: &[String]) -> Result<String, Box<dyn Error>> {
    if files.is_empty() {
        let mut text = String::new();
        io::stdin().read_to_string(&mut text)?;
        return Ok(text);
    }

    let mut text = String::new();
    for path in files {
        text.push_str(&std::fs::read_to_string(path).map_err(|e| format!("{path}: {e}"))?);
        text.push('\n');
    }
    Ok(text)
}

fn run() -> Result<(), Box<dyn Error>> {
    let options = parse_args(std::env::args().skip(1).collect())?;
    let text = read_input(&options.files)?;
    let counts = counter::count(&text);

    let width = counter::top(&counts, options.count)
        .iter()
        .map(|(_, n)| n.to_string().len())
        .max()
        .unwrap_or(1);

    for (word, n) in counter::top(&counts, options.count) {
        println!("{n:width$} {word}");
    }
    Ok(())
}

fn main() {
    if let Err(e) = run() {
        eprintln!("wf: {e}");
        process::exit(1);
    }
}
$ cargo run -q -p wf --bin wf -- -n 5 gettysburg.txt
3 and
3 nation
2 a
2 are
2 conceived

$ printf 'a b a c a b\n' | cargo run -q -p wf --bin wf -- -n 3
3 a
2 b
1 c

$ cargo run -q -p wf --bin wf -- nosuch.txt
wf: nosuch.txt: No such file or directory (os error 2)
$ echo $?
1

Three things in that program are worth pointing at.

main does nothing but call run and report. All the work is in a function that returns Result, so ? is available throughout and there is one place where errors turn into messages and an exit status. This is the standard shape for a Rust program.

Arguments are read from an iterator, and -n takes the next item from that same iterator inside the loop. std::env::args() yields the program’s own name first, which .skip(1) discards. Real programs graduate to clap at about the third option; doing it by hand once is worth the half hour.

{n:width$} takes the field width from a variable, so the counts line up whatever their magnitude. The format language has a good deal in it, and the documentation for std::fmt is where it lives.

cargo test on the finished crate runs the unit tests, the integration test, and the documentation example:

running 5 tests
test counter::tests::empty_input_has_no_words ... ok
test counter::tests::keeps_apostrophes ... ok
test counter::tests::counts_are_case_insensitive ... ok
test counter::tests::splits_on_punctuation ... ok
test counter::tests::ties_break_alphabetically ... ok

running 1 test
test counts_a_paragraph ... ok

   Doc-tests wf
running 1 test
test src/counter.rs - counter::top (line 23) ... ok

10.7 The Rest of the Toolchain

cargo fmt formats the code to the community standard. There is no style debate in Rust; run it and stop thinking about it.

cargo clippy is a lint that knows the idioms. It will tell you that a loop should be an iterator chain, that a match should be an if let, that a &String parameter should be &str. Run it before you ask anyone to read your code; early on, run it constantly, because it teaches.

cargo build --release turns on optimization. The difference against a debug build is often a factor of ten or more, so never benchmark a debug build and never ship one.

10.8 Where to Go Next

This book has covered the language a program is written in. Three areas remain, each with a reason to wait until now.

Threads are std::thread::spawn, and they are unusually approachable in Rust, because the ownership rules you already know are the rules that prevent data races. A value sent to another thread must be Send, a value shared between threads must be Sync, and the compiler checks both. Channels are in std::sync::mpsc, shared state is Arc<Mutex<T>>, and the guarantee is that the mistakes which make threaded C++ unmaintainable do not compile.

Async is a different model for a different problem: tens of thousands of tasks waiting on a network, rather than a few doing computation. async fn and .await are language features, but the runtime that drives them is a library, usually tokio. Learn threads first, and reach for async when you have a concrete reason.

unsafe marks code where you take responsibility for what the compiler would otherwise guarantee: dereferencing a raw pointer, calling into C, touching mutable global state. It does not turn off the type system, and it is not an escape hatch for arguments you lost with the borrow checker. It exists so the safe abstractions above it can be built, and most programmers never need it.

Macros you have been calling since the first page. Writing them, with macro_rules! or as procedural macros, is a real subject and can wait until you have wanted one.

For reference, the standard library documentation is the best-organized part of Rust and is available offline with rustup doc. The Rust Programming Language covers the language more slowly than this book; Programming Rust covers it in more depth; Rust for Rustaceans is what to read when the code you write starts being read by other people.

Exercise 10-1. Add a -u option to wf that counts distinct words rather than printing frequencies, and write the tests first.

Exercise 10-2. wf reads every file into one string before counting. Change it to count each file as it reads, and report per-file totals as well as the combined one.

Exercise 10-3. Add a --min LENGTH option that ignores short words. Decide what --min 0 and a negative value should do, and make the argument parser say so.

Exercise 10-4. Move parse_args into the library, give it tests, and note what had to change to make it testable.

Exercise 10-5. Write the same program in the scripting language you came from. Compare the two on length, on speed against a large file, and on what happens when the file does not exist.

Appendix A: Reference Summary

A compressed reference for the parts of Rust this book covers. The authority is the standard library documentation, available offline with rustup doc.

Keywords

as, async, await, break, const, continue, crate, dyn, else, enum, extern, false, fn, for, if, impl, in, let, loop, match, mod, move, mut, pub, ref, return, self, Self, static, struct, super, trait, true, type, unsafe, use, where, while.

Scalar Types

TypeMeaning
i8 i16 i32 i64 i128signed integers, default i32
u8 u16 u32 u64 u128unsigned integers
isize usizepointer-sized; usize indexes and measures
f32 f64floating point, default f64
booltrue or false, nothing converts to it
charone Unicode scalar value, four bytes
()the unit, value of an expression with no value
!never returns (return, break, panic!)

Compound Types

WrittenIs
(T, U)tuple, fields by position
[T; N]array, length in the type, no heap
&[T], &mut [T]slice, a borrowed view
Vec<T>growable array on the heap
String, &strowned and borrowed UTF-8 text
Option<T>Some(T) or None
Result<T, E>Ok(T) or Err(E)
Box<T>owned value on the heap
HashMap<K, V>, BTreeMap<K, V>maps, unordered and sorted
HashSet<T>, BTreeSet<T>sets, unordered and sorted

Ownership in One Table

WrittenMeans
x: Towns it; caller loses it when passed
x: &Tborrows to read; many may exist at once
x: &mut Tborrows to modify; only one may exist
x.clone()a deep copy, explicit and possibly expensive
move || ...closure takes ownership of what it captures
'aa lifetime: a region for which a reference is valid
'staticvalid for the whole program; every literal is

Formatting

println!, print!, eprintln!, format!, and write! share one language.

FormResult
{}Display, the user-facing form
{:?} {:#?}Debug, compact and multi-line
{name}the variable of that name
{:.3}three digits after the point
{:10.3} {:<10.3} {:^10.3}width ten: right, left, centered
{:width$}width taken from a variable
{:08}zero-padded to eight
{:+}always show the sign
{:b} {:o} {:x} {:X} {:#x}binary, octal, hex, upper hex, prefixed
{:e}scientific notation
{{ }}literal braces

Escapes inside string literals are \n, \t, \r, \0, \\, \", \', \u{1F980}. A raw string r"..." or r#"..."# takes no escapes at all.

Option and Result

MethodOn None / Err
unwrap()panics
expect("why")panics with your message
unwrap_or(v)gives v
unwrap_or_else(|| v)computes v only if needed
unwrap_or_default()gives the type’s default
map(f)leaves it alone
and_then(f)leaves it alone; f returns the same wrapper
ok_or(e)turns None into Err(e)
ok()turns Err into None
?returns it from the enclosing function

Iterators

Starting one: .iter() gives &T, .iter_mut() gives &mut T, .into_iter() consumes and gives T. A range a..b or a..=b is one, and so is a.. without end.

Adapters, which do nothing until consumed: map, filter, filter_map, take, take_while, skip, skip_while, step_by, rev, zip, enumerate, chain, flat_map, flatten, peekable, inspect, cloned, copied.

Consumers, which run the chain: collect, sum, product, count, fold, for_each, min, max, min_by_key, max_by_key, any, all, find, position, last, nth, partition.

Strings

Measuring is len() in bytes and chars().count() in characters. Walking is chars(), bytes(), char_indices(), lines(), split_whitespace(). Splitting is split, splitn, split_once, rsplit. Trimming is trim, trim_start, trim_end, trim_matches. Testing is contains, starts_with, ends_with, find, is_empty. Changing is to_lowercase, to_uppercase, replace, repeat, push_str, push. Converting is parse, to_string, as_str.

All indices are byte offsets, and slicing between character boundaries panics.

Attributes

AttributeEffect
#[derive(Debug, Clone, ...)]the compiler writes the implementation
#[test]a test function
#[cfg(test)]compile only when testing
#[should_panic]this test passes by panicking
#[allow(dead_code)]silence one lint here
#[must_use]warn if the return value is discarded
/// //!documentation for the next item, and for this module

Cargo

CommandDoes
cargo new, cargo new --libstart a program, start a library
cargo run, cargo run --bin xbuild and run
cargo build --releaseoptimized build
cargo checktype-check without producing a binary, much faster
cargo test, cargo test namerun tests, run matching tests
cargo fmtformat to the standard style
cargo clippylint, and learn the idioms
cargo doc --openbuild and read the documentation
cargo add nameadd a dependency
cargo treeshow what you have pulled in
rustc --explain E0382explain an error code

Appendix B: Coming From Another Language

From Lux

Lux was built to be outgrown, and Rust is one of the three languages it was built to be outgrown into. Most of what you learned transfers without translation, because Lux took its data model from the same family Rust belongs to: values that hold still unless you say otherwise, no null, a match that has to cover every case, and a failure that is a value rather than a hidden mechanism.

LuxRust
let x = 5let x = 5;
var x = 5let mut x = 5;
int, float, string, booli64 (or i32), f64, String, bool
[int]Vec<i64>, or &[i64] when borrowed
func f(n: int) -> int { }fn f(n: i64) -> i64 { }
return n * 2n * 2 as the last line, or return n * 2;
print("x is", x)println!("x is {x}")
length(xs)xs.len()
queue += 4queue.push(4);
for x in xsfor x in &xs
for i in 0..5for i in 0..5
struct Point { x: int }struct Point { x: i64 }
Point(x: 3, y: 4)Point { x: 3, y: 4 }
enum Shape { circle(radius: float) }enum Shape { Circle(f64) }
Shape.circle(radius: 2.0)Shape::Circle(2.0)
match s { circle(let r) => ... }match s { Shape::Circle(r) => ..., }
some(x), noneSome(x), None
ok(x), err(reason)Ok(x), Err(reason)
Option<int>, Result<int, string>Option<i64>, Result<i64, String>
int(3.9), string(42)3.9 as i64, 42.to_string()
parseInt(s)s.parse::<i64>(), which gives a Result
readFile(path)std::fs::read_to_string(path)
eprint(msg)eprintln!("{msg}")
func main() { }fn main() { }, and it is required

The mechanical differences are semicolons at the ends of statements, commas between match arms, :: where Lux writes a dot for a namespace, CamelCase variant names, and the type names being explicit about width. Fix those and most Lux programs are nearly Rust.

Four differences are real.

Ownership. Lux copies values; assigning an array gives you an independent array. Rust moves them, and the original becomes unusable. This is Chapter 5, and it is the largest single difference between the two languages. When it frustrates you, .clone() is the Lux behavior, spelled out loud.

Mutation is one word, and it is a different word. var becomes let mut, and mut also appears on parameters, on references, and on methods. Lux asks whether a name can be reassigned; Rust asks that, and also who is allowed to modify a value through a borrow.

parseInt gives an Option, parse gives a Result. Rust tells you why the parse failed. Use .ok() when you want the Lux shape.

Strings are bytes underneath. length(s) in Lux counts characters, and s.len() in Rust counts bytes. Use s.chars().count() for the Lux answer. Chapter 7 explains why Rust makes you choose.

You also have a tool. lux convert rust program.lux prints the Rust translation of any Lux program you have written, and comparing the two is a fast way to learn the mapping in the terms of code you already understand. The output is deliberately literal, and it clones more than a Rust programmer would, for the reason Chapter 5 gives.

From Python

PythonRust
x = 5let mut x = 5;, or let x = 5; when it never changes
def f(n): return n * 2fn f(n: i64) -> i64 { n * 2 }
print(f"x is {x}")println!("x is {x}");
[1, 2, 3]vec![1, 2, 3]
{"a": 1}HashMap::from([("a", 1)])
{1, 2}HashSet::from([1, 2])
(1, "two")(1, "two")
for x in xs:for x in &xs {
[f(x) for x in xs if p(x)]xs.iter().filter(|x| p(x)).map(|x| f(x)).collect()
len(xs)xs.len()
xs.append(v)xs.push(v)
d.get(k)d.get(&k), an Option
NoneNone, but the type must say Option<T>
try / exceptResult and ?
raisereturn Err(...), or panic! for a bug
with open(p) as f:scope ends, the file closes itself
class C:struct C plus impl C
isinstance(x, C)usually an enum and a match
int("42")"42".parse::<i64>(), a Result
# type: ignoreno equivalent, and that is the point

The adjustments that take longest: every type is fixed at compile time, integers do not grow without bound, a dictionary lookup gives you an Option rather than a KeyError, and there is no truthiness.

From JavaScript

JavaScriptRust
const x = 5let x = 5;
let x = 5let mut x = 5;
function f(n) { return n * 2 }fn f(n: i64) -> i64 { n * 2 }
n => n * 2|n| n * 2
[1, 2, 3]vec![1, 2, 3]
{a: 1} as a recorda struct
{} as a lookup tablea HashMap
xs.map(f).filter(p)xs.iter().map(f).filter(p).collect()
xs.lengthxs.len()
null and undefinedOption<T>, and there is only one of it
try / catchResult and ?
throwreturn Err(...), or panic! for a bug
=====, which is the only kind there is
JSON.parseserde_json, a dependency
async / awaitasync / await, with a runtime you choose
npm installcargo add

The adjustments: numbers are not all floats, == compares values of the same type and refuses anything else, there is no implicit conversion anywhere, and the absence of a value has one spelling rather than two.

Appendix C: Reading Compiler Errors

Rust’s diagnostics are a feature of the language, not an afterthought. Each one names the error code, states the problem in a sentence, marks the source spans that caused it, and often writes the correction. rustc --explain E0382 prints a page on any code.

Read them from the top and change one thing at a time. The errors below are the ones you will meet in the first month. Each one has a program that provokes it in examples/errors, named for the mistake, so you can produce any of these messages yourself with rustc assign_twice.rs and then try the fix.

E0384: cannot assign twice to immutable variable

error[E0384]: cannot assign twice to immutable variable `fahr`
2 |     let fahr = 0;
  |         ---- first assignment to `fahr`
3 |     fahr = fahr + 20;
  |     ^^^^^^^^^^^^^^^^ cannot assign twice to immutable variable
help: consider making this binding mutable
2 |     let mut fahr = 0;

You assigned to a let. Add mut, or, if the value is being refined rather than changed, declare a new one with the same name and let it shadow.

E0596: cannot borrow as mutable

error[E0596]: cannot borrow `v` as mutable, as it is not declared as mutable
3 |     v.push(2);
  |     ^ cannot borrow as mutable
help: consider changing this to be mutable
2 |     let mut v = vec![1];

The same problem seen through a method. push needs &mut self and the variable is not mut.

E0308: mismatched types

error[E0308]: mismatched types
1 | fn double(n: i32) -> i32 {
  |    ------            ^^^ expected `i32`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
2 |     n * 2;
  |          - help: remove this semicolon to return this value

The commonest form of this is a stray semicolon on the last line of a function, which turns the value into nothing. The general form is that two types met and did not agree, and the message says which was expected and which arrived. When the two are numeric, the cause is Rust not converting for you: add as, or i64::from(...), or pick matching types.

E0425: cannot find value

A name is not in scope. Check the spelling, check whether it needed a use line, and check whether it was declared in a block that has already ended.

E0004: non-exhaustive patterns

error[E0004]: non-exhaustive patterns: `i32::MIN..=0_i32` and `3_i32..=i32::MAX` not covered
3 |     let name = match n {
  |                      ^ patterns not covered

A match left a case out. Either handle it or add _ => .... When this fires on your own enum after you added a variant, that is the feature working: the compiler is listing the places that now need attention.

E0277: trait bound not satisfied

error[E0277]: `Point` doesn't implement `Debug`
7 |     println!("{p:?}");
  |               ^^^^^ `Point` cannot be formatted using `{:?}` because it doesn't implement `Debug`
  = help: the trait `Debug` is not implemented for `Point`
help: consider annotating `Point` with `#[derive(Debug)]`

Something was asked to do a thing its type cannot do. Printing a struct that does not derive Debug is the usual case, and the help line is the fix. In generic code, the same error means a missing bound on the type parameter. Note that the message also appears for mixed arithmetic: cannot multiply {integer} by {float} is this error wearing different clothes, because * is a trait.

E0382: use of moved value

error[E0382]: use of moved value: `h.name`
   --> moved_field.rs:12:18
   |
11 |     let n = take(h.name);
   |                  ------ value moved here
12 |     let m = take(h.name);
   |                  ^^^^^^ value used here after move
   |
   = note: move occurs because `h.name` has type `String`, which does not implement the `Copy` trait

A value was handed away and then used again. Four fixes, in the order to consider them. Pass a reference instead, if the function only needs to read. Have the function hand the value back. Restructure so the value is used once. Or .clone() it, which is correct and costs a copy.

E0507: cannot move out of borrowed content

error[E0507]: cannot move out of `r.name` which is behind a shared reference
 --> move_out_of_ref.rs:8:25
  |
8 |     let taken: String = r.name;
  |                         ^^^^^^ move occurs because `r.name` has type `String`, which does not implement the `Copy` trait
  |
help: consider borrowing here
  |
8 |     let taken: String = &r.name;
  |                         +
help: consider cloning the value if the performance cost is acceptable
  |
8 |     let taken: String = r.name.clone();
  |                               ++++++++

You have a borrow and tried to take ownership of something inside it. You cannot empty out something you do not own. Borrow the field instead, or clone it, or take the whole structure by value if that is what you meant.

E0502: cannot borrow as mutable because also borrowed as immutable

error[E0502]: cannot borrow `names` as mutable because it is also borrowed as immutable
3 |     let first = &names[0];
  |                  ----- immutable borrow occurs here
4 |     names.push(String::from("grace"));
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
5 |     println!("{first}");
  |                ----- immutable borrow later used here

Something is being modified while something else is looking at it. The third span is the one to attend to: the borrow lives until its last use, so moving that use earlier, or ending it before the modification, often fixes the program as it stands. If the loop genuinely needs to modify what it is walking, collect the changes and apply them afterwards.

E0499: cannot borrow as mutable more than once

error[E0499]: cannot borrow `v` as mutable more than once at a time
3 |     let a = &mut v;
  |             ------ first mutable borrow occurs here
4 |     let b = &mut v;
  |             ^^^^^^ second mutable borrow occurs here

One mutable borrow at a time. Usually the second one is not needed; use the first. When two parts of a structure really are needed at once, take them from different fields, or use split_at_mut on a slice.

E0106: missing lifetime specifier

error[E0106]: missing lifetime specifier
1 | fn longest(a: &str, b: &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 `a` or `b`
help: consider introducing a named lifetime parameter
1 | fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {

The function returns a reference and the compiler cannot tell what it borrows from. Take the suggested annotation. When the message instead says there is no value for the result to be borrowed from, you are returning a reference to a local, and the fix is to return an owned value.

E0597: does not live long enough

error[E0597]: `inner` does not live long enough
4 |         let inner = String::from("hello");
  |             ----- binding `inner` declared here
5 |         outer = &inner;
  |                 ^^^^^^ borrowed value does not live long enough
6 |     }
  |     - `inner` dropped here while still borrowed
7 |     println!("{outer}");
  |                ----- borrow later used here

A reference outlived what it pointed at. The four spans give the sequence: where the value was made, where it was borrowed, where it died, and where the dead reference was used. Either move the value out to the longer scope or store an owned copy rather than a reference.

Warnings

Warnings are worth the same attention as errors. An unused variable often means a line that was supposed to use it. An unused Result means an error being dropped on the floor. An unused mut means the mutation you thought you were doing is not happening. cargo clippy adds a few hundred more, and early on it is the cheapest teacher available.