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:
| Level | Operators |
|---|---|
| 1 | paths ::, method calls, field access, indexing [] |
| 2 | ? |
| 3 | unary - ! * & &mut |
| 4 | as |
| 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.
| Trait | What 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() |
Hash | use as a HashMap key |
Default | Type::default() |
From, Into | conversions, and ? |
Iterator | for loops and every adapter |
Drop | cleanup 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
| Type | Meaning |
|---|---|
i8 i16 i32 i64 i128 | signed integers, default i32 |
u8 u16 u32 u64 u128 | unsigned integers |
isize usize | pointer-sized; usize indexes and measures |
f32 f64 | floating point, default f64 |
bool | true or false, nothing converts to it |
char | one Unicode scalar value, four bytes |
() | the unit, value of an expression with no value |
! | never returns (return, break, panic!) |
Compound Types
| Written | Is |
|---|---|
(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, &str | owned 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
| Written | Means |
|---|---|
x: T | owns it; caller loses it when passed |
x: &T | borrows to read; many may exist at once |
x: &mut T | borrows to modify; only one may exist |
x.clone() | a deep copy, explicit and possibly expensive |
move || ... | closure takes ownership of what it captures |
'a | a lifetime: a region for which a reference is valid |
'static | valid for the whole program; every literal is |
Formatting
println!, print!, eprintln!, format!, and write! share one language.
| Form | Result |
|---|---|
{} | 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
| Method | On 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
| Attribute | Effect |
|---|---|
#[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
| Command | Does |
|---|---|
cargo new, cargo new --lib | start a program, start a library |
cargo run, cargo run --bin x | build and run |
cargo build --release | optimized build |
cargo check | type-check without producing a binary, much faster |
cargo test, cargo test name | run tests, run matching tests |
cargo fmt | format to the standard style |
cargo clippy | lint, and learn the idioms |
cargo doc --open | build and read the documentation |
cargo add name | add a dependency |
cargo tree | show what you have pulled in |
rustc --explain E0382 | explain 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.
| Lux | Rust |
|---|---|
let x = 5 | let x = 5; |
var x = 5 | let mut x = 5; |
int, float, string, bool | i64 (or i32), f64, String, bool |
[int] | Vec<i64>, or &[i64] when borrowed |
func f(n: int) -> int { } | fn f(n: i64) -> i64 { } |
return n * 2 | n * 2 as the last line, or return n * 2; |
print("x is", x) | println!("x is {x}") |
length(xs) | xs.len() |
queue += 4 | queue.push(4); |
for x in xs | for x in &xs |
for i in 0..5 | for 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), none | Some(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
| Python | Rust |
|---|---|
x = 5 | let mut x = 5;, or let x = 5; when it never changes |
def f(n): return n * 2 | fn 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 |
None | None, but the type must say Option<T> |
try / except | Result and ? |
raise | return 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: ignore | no 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
| JavaScript | Rust |
|---|---|
const x = 5 | let x = 5; |
let x = 5 | let 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 record | a struct |
{} as a lookup table | a HashMap |
xs.map(f).filter(p) | xs.iter().map(f).filter(p).collect() |
xs.length | xs.len() |
null and undefined | Option<T>, and there is only one of it |
try / catch | Result and ? |
throw | return Err(...), or panic! for a bug |
=== | ==, which is the only kind there is |
JSON.parse | serde_json, a dependency |
async / await | async / await, with a runtime you choose |
npm install | cargo 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.