Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.