Skip to content

Rust Variables, Types, Functions and Control Flow

Use Rust variables to name data, typed functions to transform it, and conditions to choose a result. In this Raspberry Pi tutorial, you will classify fixed temperature readings and test the threshold before working with real sensors.

Before you start

Complete the first Cargo lesson. This lesson needs a working Rust toolchain, not GPIO equipment or a Raspberry Pi: all readings are invented fixture data.

The example uses millidegrees Celsius, the unit used by Linux thermal-zone readings. A value of 46,700 represents 46.7 degrees Celsius. Our 60,000 threshold is an exercise rule, not a Raspberry Pi throttling limit or cooling recommendation.

Run a complete Rust example

Create a separate project so you can keep the first lesson:

cargo new pi_readings
cd pi_readings

Replace src/main.rs with this complete program:

// An exercise threshold, not a hardware safety limit.
const NOTICE_MILLIDEGREES: i32 = 60_000;

fn needs_notice(millidegrees: i32) -> bool {
    millidegrees >= NOTICE_MILLIDEGREES
}

fn main() {
    // Fixed data: this program does not read the Pi's sensors.
    let readings: [i32; 3] = [46_700, 60_000, 63_200];
    let mut notice_count: usize = 0;

    for millidegrees in readings {
        let celsius = f64::from(millidegrees) / 1_000.0;
        let notice = needs_notice(millidegrees);
        let label = if notice { "notice" } else { "below threshold" };

        if notice {
            notice_count += 1;
        }

        println!("{celsius:.1} C: {label}");
    }

    println!("Readings at or above the exercise threshold: {notice_count}");
}

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

    #[test]
    fn below_threshold_does_not_need_notice() {
        assert!(!needs_notice(59_999));
    }

    #[test]
    fn threshold_itself_needs_notice() {
        assert!(needs_notice(60_000));
    }

    #[test]
    fn above_threshold_needs_notice() {
        assert!(needs_notice(60_001));
    }
}

Run it:

cargo run --quiet

Expected program output:

1
2
3
4
46.7 C: below threshold
60.0 C: notice
63.2 C: notice
Readings at or above the exercise threshold: 2

Rust variables and types: name the units

let creates a binding. Bindings are immutable by default; let mut permits reassignment. Here, the counter changes as the loop processes readings, but the readings array does not.

The annotations make the data contract visible:

Type Role in this example
i32 Signed integer reading in millidegrees
[i32; 3] Exactly three readings of the same type
usize Counter using the platform's pointer-sized unsigned integer type
f64 Floating-point value used for display
bool The decision returned by needs_notice

The compiler infers the types of celsius and notice from their expressions. Underscores in numeric literals improve readability; 60_000 and 60000 are the same value.

Keep the decision in integer units and convert only for display. Integer division such as 46_700 / 1_000 discards the fractional part; converting first avoids that problem. The formatting expression {celsius:.1} displays one decimal place.

Functions return a value, not just printed text

fn needs_notice(millidegrees: i32) -> bool declares one typed parameter and a Boolean return value. Its final comparison has no semicolon, so that expression becomes the returned value.

Adding a semicolon after the comparison turns it into a statement; without another return expression, this function would no longer return the required Boolean. Printing a result with println! also does not return that result to the caller.

Separating the decision from printing lets tests call the same function without capturing terminal output. See the official Rust functions chapter for expressions and return values.

Conditions and loops choose what happens

if notice requires a Boolean condition. Rust does not interpret a nonzero integer as true.

The if used to assign label is also an expression: it chooses one of two string literals. Both branches need compatible types. The for loop processes each array element once, while the separate conditional increments the counter only for matching readings.

Use for when traversing a known collection. Later, while can express repetition while a condition holds, and loop can express repetition until an explicit exit. We do not need either for three fixtures. Read the official control-flow chapter for those alternatives.

Verify the boundary, then change the rule

1
2
3
cargo check
cargo test
cargo fmt --check

The supplied tests cover one value immediately below the threshold, the threshold itself, and one immediately above it. Expect three passing tests. #[cfg(test)] includes the module for testing, #[test] marks each test, and use super::needs_notice makes the parent module's function available inside it. Modules are explained more fully later.

Now change >= to > and rerun the tests. The equality test should fail: that failure exposes a changed boundary rule. Restore >= afterward.

These tests check classification, not sensor accuracy, the printed output, permissions, or hardware behaviour. A compiler check alone does not establish those either.

Try mutability and shadowing separately

Inside main, try this short experiment, then remove it:

1
2
3
let reading = 46_700;
let reading = f64::from(reading) / 1_000.0;
println!("Converted fixture: {reading:.1} C");

Repeating let creates a new binding that shadows the earlier one, allowing a different type. In contrast, assigning a floating-point value to an existing mutable integer binding is a type error. Prefer different names when they make units clearer, as the complete example does. See variables and mutability.

Common problems

  • Cannot assign to an immutable variable: check whether the counter was declared with mut.
  • Expected bool, found (): check the final expression in needs_notice, including its semicolon.
  • No tests run: ensure the test module is saved in the project's src/main.rs and each function has #[test].
  • Formatting check fails: run cargo fmt, then repeat the check. If the component is missing in a rustup installation, use rustup component add rustfmt.

What comes next

Verification environment

On October 10, 2026, the complete example above was compiled and executed on a Raspberry Pi 4 Model B with 64-bit user space, kernel 6.18.50+rpt-rpi-v8, Rust 1.99.0, and Cargo 1.99.0 installed. Program output matched the example, all three boundary tests passed, and the rustfmt check passed.

For this verification, the article's code was supplied directly to rustc --edition=2024 and rustc --edition=2024 --test; the Cargo commands above were not separately exercised. This establishes native execution of the example, not live temperature acquisition or performance.

You now have a data-to-decision function and explicit boundary tests. Keep hardware access out of this example: reading a Linux file introduces failures and parsing that deserve their own treatment.

Continue with types, operators and expressions, then ownership and borrowing. Integer copying in this example does not explain what happens when a program passes an owned String between functions.

Previous: toolchain and Cargo · Rust course overview

Donate