R
RUSTY YELLOW PAGES

Expression-oriented languageConcept


Explanation

In Rust, almost everything evaluates to a value: if/else, match, loop, and even a plain { } block are expressions, not statements. This is a different default from C-family languages, where if only decides which branch of code runs and never produces a value itself. In Rust, if condition { a } else { b } is a value — usable directly as the right-hand side of a let, a function argument, or the tail of another block.

This matters because it removes an entire category of boilerplate: rather than declaring a variable, then mutating it inside each branch of an if/else or match, you write the branches so each one produces the final value directly, and bind it once. The same idea applies to function bodies — a function's final expression, with no trailing semicolon, is its return value, which is why idiomatic Rust functions rarely need an explicit return.

The semicolon is what separates the two: appending ; turns an expression into a statement, discarding whatever value it produced (an expression-statement's value becomes (), unit). This is also why adding or removing a trailing semicolon on the last line of a function body changes what the function returns — a common early surprise, and a direct consequence of the expression/statement distinction rather than an arbitrary rule.

Closures follow the same rule as functions: a closure's body is an expression too, which is why a short closure can be written on one line without braces at all — see Closures & capturing. Being expression-oriented is also one of the traits Rust shares with functional languages, alongside pattern matching and immutability by default, even though Rust itself is not a purely functional language.

Basic usage example

let score = 82;
let grade = if score >= 90 { "A" } else if score >= 80 { "B" } else { "C" };
// <- `if`/`else` is an expression here: it produces `grade` directly, no `mut` needed

Best practices & deeper information

Scenario

Branching on data (pattern matching)

Computing a shipping fee from an order's weight tier is a direct value computation, so match is written as an expression that produces the fee, rather than a mut variable assigned inside each arm.

enum WeightTier {
    Light,
    Standard,
    Heavy,
}

fn shipping_fee_cents(tier: &WeightTier) -> u32 {
    match tier { // <- the whole `match` evaluates to a value; the function just returns it
        WeightTier::Light => 300,
        WeightTier::Standard => 700,
        WeightTier::Heavy => 1500,
    }
}

Why this way: every arm must produce a value of the same type, which the Rust Reference documents as part of match's grammar as an expression — the compiler rejects a match where the arms' value types disagree, catching the mistake before it ships.

Scenario

Validating input

Validating a raw string into a plausible age produces either an Ok or an Err value; writing that as an if/else expression keeps the result value's construction next to the condition that decides it.

fn validate_age(raw: &str) -> Result<u8, String> {
    let age: u8 = raw.trim().parse().map_err(|_| "not a number".to_string())?;

    if age == 0 || age > 130 { // <- `if` as an expression: it produces the Err/Ok value directly
        Err(format!("{age} is not a plausible age"))
    } else {
        Ok(age)
    }
}

Why this way: letting the if/else produce the Result directly avoids a separate mut "result" variable reassigned in each branch — the kind of unnecessary mutability the Rust Design Patterns' temporary-mutability idiom recommends avoiding in favor of scoping the computation to an expression.

Scenario

Creating a new object

Building a Config where one field depends on a flag reads more clearly when that field is computed by an if expression bound once, rather than declared mut and reassigned before the struct literal.

struct Config {
    retries: u32,
    timeout_ms: u32,
}

fn build_config(verbose: bool) -> Config {
    let timeout_ms = if verbose { 10_000 } else { 3_000 };
    // <- expression assigns the final value directly; `timeout_ms` is never `mut`

    Config {
        retries: 3,
        timeout_ms,
    }
}

Why this way: binding the result of an expression once, instead of mutating a placeholder, keeps every binding's lifetime as short and as immutable as possible — a pattern the Rust Design Patterns book calls out directly as an idiom to prefer over temporary mutability.

Explanation

Embedded support: Full

There is genuinely little to add here beyond: it's still true, and used exactly the same way. Being expression-oriented is a property of Rust's grammar, resolved entirely at compile time — if/match/block expressions produce values and lower to the same branching code whether the target is a hosted server or a microcontroller with no std, no allocator, and no OS. Embedded code leans on this just as much as any other Rust code, most visibly for picking a configuration constant based on a compile-time or boot-time condition (a baud rate, a clock-prescaler value, a register bit pattern) in one expression, rather than declaring a mut variable and assigning it in each branch.

Basic usage example

const HIGH_SPEED: bool = true;
let baud_rate = if HIGH_SPEED { 115_200 } else { 9_600 };
// <- `if`/`else` is an expression here: it produces `baud_rate` directly, no `mut` needed

Best practices & deeper information

Scenario

Creating a new object

Building a UART configuration where the baud rate depends on which crystal frequency the board was fitted with reads more clearly when that field is computed by an if expression bound once, rather than declared mut and reassigned before the struct literal — exactly the same idiom as in hosted code, just choosing a hardware constant instead of an app setting.

struct UartConfig {
    baud_rate: u32,
    data_bits: u8,
}

fn build_uart_config(high_speed_crystal: bool) -> UartConfig {
    let baud_rate = if high_speed_crystal { 115_200 } else { 9_600 };
    // <- expression assigns the final value directly; `baud_rate` is never `mut`

    UartConfig {
        baud_rate,
        data_bits: 8,
    }
}

Why this way: binding the result of an expression once, instead of mutating a placeholder, keeps the binding as short-lived and immutable as possible — the same Rust Design Patterns temporary-mutability idiom cited in this page's classic Best practices applies unchanged to a hardware-configuration value.