Operators and Expressions

Expression-Based Programming

Rust is primarily an expression-based language. Rather than treating control flow (if, match, loop) and blocks ({ ... }) as statements that only mutate state via side effects, Rust treats almost every construct as a value-producing expression that can be composed, assigned to an immutable variable, passed as an argument, or returned directly from a function.

Imperative Statement Style vs Expression-Oriented Style

Compare how a multi-branch calculation is structured in a traditional statement-oriented style versus idiomatic expression-oriented Rust:

Statement-Oriented Style:                 Expression-Oriented Style (Idiomatic Rust):
  let mut price;                            let price = if is_member {
  if is_member {                                base_price - discount
      price = base_price - discount;        } else {
  } else {                                      base_price
      price = base_price;                   };
  }
  - Requires `mut` or delayed init          - `price` is initialized once & immutable
  - Risk of forgetting a branch             - Compiler enforces every branch returns a value
fn compute_shipping_cents(weight_grams: u32, express: bool) -> u32 {
    let base_rate = match weight_grams {
        0 => return 0,
        1..=500 => 499,
        501..=2000 => 899,
        _ => 1499,
    };

    let multiplier = if express { 2 } else { 1 };

    base_rate * multiplier
}

fn main() {
    println!("0g standard:    {} cents", compute_shipping_cents(0, false));
    println!("300g standard:  {} cents", compute_shipping_cents(300, false));
    println!("1200g express:  {} cents", compute_shipping_cents(1200, true));
}
0g standard:    0 cents
300g standard:  499 cents
1200g express:  1798 cents

Everything That Is an Expression in Rust

In Rust, only three constructs are statements: let declarations, item declarations (fn, struct, use, const), and expression statements (expr;). Almost everything else is an expression with a concrete type:

Expression Category Examples Resulting Type
Literals & Paths 42, "hi", std::f64::consts::PI i32, &str, f64
Operators & Calls a + b, vec.len(), parse(s)? Output type of operator or function
Block Expression { let x = 2; x * 10 } Type of tail expression (i32), or () if ending with ;
Branching (if / match) if c { a } else { b } / match v { ... } Common type of all branches/arms
Breakable loop / 'b: {} loop { break 42; } / 'b: { break 'b 7; } Type of break value (i32)
Side-Effect Loops & Assigns while c {}, for x in i {}, a = 5 Unit type ()
Diverging Jumps return, break, continue, panic!() Never type ! (coerces into any type!)

Nesting Expressions Inside Struct Initializers and Function Calls

Because blocks, if/else, and match are expressions, they can appear directly inside struct field initializers, array literals, or function argument lists:

#[derive(Debug)]
struct ServerSettings {
    host: &'static str,
    port: u16,
    workers: usize,
}

fn build_settings(production: bool) -> ServerSettings {
    ServerSettings {
        host: if production { "0.0.0.0" } else { "127.0.0.1" },
        port: match production {
            true => 443,
            false => 8080,
        },
        workers: {
            let base_cores = 4;
            if production { base_cores * 2 } else { 1 }
        },
    }
}

fn main() {
    println!("Dev:  {:?}", build_settings(false));
    println!("Prod: {:?}", build_settings(true));
}
Dev:  ServerSettings { host: "127.0.0.1", port: 8080, workers: 1 }
Prod: ServerSettings { host: "0.0.0.0", port: 443, workers: 8 }

Chaining Combinators and Iterator Expressions

Expression-based programming extends beyond built-in language syntax into standard library types like Option<T>, Result<T, E>, and Iterator. Using combinator methods (.map(), .and_then(), .unwrap_or(), .filter(), .fold()) transforms values through a pipeline of expressions without intermediate mutable variables:

fn parse_and_clamp_score(raw: Option<&str>) -> u32 {
    raw.map(str::trim)
        .filter(|s| !s.is_empty())
        .and_then(|s| s.parse::<u32>().ok())
        .map(|score| score.min(100))
        .unwrap_or(0)
}

fn main() {
    for input in [Some(" 85 "), Some(" 150 "), Some("invalid"), Some(""), None] {
        println!("{input:?} -> {}", parse_and_clamp_score(input));
    }
}
Some(" 85 ") -> 85
Some(" 150 ") -> 100
Some("invalid") -> 0
Some("") -> 0
None -> 0

Benefits of Expression-Based Programming

  1. Immutability by default: Variables are bound directly to the result of a block, if, or match expression, eliminating the need for let mut during setup.
  2. Type-checked completeness: Every branch of a value-producing if or match is verified by the compiler to produce the same type (or diverge with !), making it impossible to forget to assign a variable in one branch.
  3. Tight variable scopes: Helper variables used to compute an expression live inside { ... } blocks and disappear immediately once the expression produces its final value.