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.