Rust / the practical course

01 / Foundations

Values, functions, and control flow

Write functions, choose between cases, and handle numbers that reach their limits.

45 min + practiceRust 1.98.1 · Edition 2024

By Robert DeVore · Download Markdown

Write your first function

Our first question is whether a request took less than 100 milliseconds. A function gives that rule a name. milliseconds: u32 names a parameter and its unsigned 32-bit integer type. -> &'static str says the result is a reference to text stored for the duration of the program; here both answers are string literals. We will unpack references later.

Run the example

cargo run --locked --example 02_values
fn classify(milliseconds: u32) -> &'static str {
    if milliseconds < 100 { "fast" } else { "slow" }
}
fn main() {
    let samples = [12, 100, 340];
    for value in samples {
        println!("{value}: {}", classify(value));
    }
    assert_eq!(classify(100), "slow");
    assert_eq!(u8::MAX.checked_add(1), None);
}

View the tested source

if produces a value, so both branches must agree on its type. The final expression in the function has no semicolon: its value is returned. Add a semicolon and the block instead evaluates to (), the unit value, unless an explicit return supplies the function result. This is a common first compiler error.

let samples = [12, 100, 340] creates an array. A for loop visits its values. Formatting captures value by name; {} receives the following expression. assert_eq! checks a fact and panics if it is false. These assertions check that the example does what we expect.

Bindings and changes

A binding is immutable unless declared mut. Use mut when you intend to change the binding. Shadowing with another let creates a new binding; it can have a different type. Mutation updates an existing place and preserves its type. Neither operation is an ownership workaround: moving a non-Copy value still matters, as the next lesson shows.

Booleans are true and false; conditions do not treat arbitrary integers as booleans. Use &&, ||, and ! to combine predicates. A range 0..3 excludes 3; 0..=3 includes it. A match chooses among patterns and must cover every possible input, either explicitly or with a fallback. Prefer it when several distinct cases deserve separate handling.

Choose a numeric policy

An integer type has a finite range. Do not write a production counter assuming overflow can never happen. Ordinary arithmetic can panic when overflow checks are enabled, while unchecked release arithmetic generally wraps. Select checked_add for an explicit failure, saturating_add for a deliberate upper bound, or wrapping_add for modular arithmetic. Choose the behavior your program needs. A passing debug build does not settle that choice.

usize is useful for indexing and sizes in memory; its width depends on the target. It is not automatically the right type for a portable file format. Floating-point values cannot represent many real numbers exactly. If you use them, decide how to handle equality, rounding during sums, infinity, and NaN. Our capstone uses integer counts.

Exercise

Change the classifier to return fast below 100, acceptable from 100 through 499, and slow at 500 or above. Add assertions for 99, 100, 499, and 500. Then deliberately put a semicolon after the final if expression and read the error before removing it.

Solution and acceptance check

Use if milliseconds < 100 { "fast" } else if milliseconds < 500 { "acceptable" } else { "slow" }. The four tests exercise both sides of each boundary. The return annotation demands text; the extra semicolon produces unit. Fix the value-producing expression, not the return type, because the function's job is to classify.

See primitive numeric types, control flow, and statements and expressions.

Find a lesson