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
- Immutability by default: Variables are bound directly to the result of a block,
if, ormatchexpression, eliminating the need forlet mutduring setup. - Type-checked completeness: Every branch of a value-producing
iformatchis verified by the compiler to produce the same type (or diverge with!), making it impossible to forget to assign a variable in one branch. - Tight variable scopes: Helper variables used to compute an expression live inside
{ ... }blocks and disappear immediately once the expression produces its final value.