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:
Replace src/main.rs with this complete program:
Run it:
Expected program output:
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¶
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:
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 inneeds_notice, including its semicolon. - No tests run: ensure the test module is saved in the project's
src/main.rsand each function has#[test]. - Formatting check fails: run
cargo fmt, then repeat the check. If the component is missing in a rustup installation, userustup 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.