Functions and Control Flow
Prerequisites: Variables and Types
Next: Ownership
This article covers how to define functions, use Rust's expression-based control flow, and iterate over ranges and collections.
Functions
def add(a: int, b: int) -> int:
return a + b
result = add(3, 4) # 7
fn add(a: i32, b: i32) -> i32 {
a + b // no semicolon = implicit return (expression value)
}
let result = add(3, 4); // 7
Implicit return. The last expression in a block without a ; is the return value. Adding ; turns it into a statement that returns () (unit — Rust's equivalent of None). Both are idiomatic; use return only for early exits.
Multiple Return Values
def model_metrics(preds, labels):
return compute_precision(preds, labels), compute_recall(preds, labels)
p, r = model_metrics(preds, labels)
fn model_metrics(preds: &[f64], labels: &[f64]) -> (f64, f64) {
let precision = compute_precision(preds, labels);
let recall = compute_recall(preds, labels);
(precision, recall)
}
let (p, r) = model_metrics(&preds, &labels);
For more than two or three values, prefer a named struct over a tuple — it documents what each field means.
No Default Arguments (But Workarounds Exist)
def ingest(lines, num_threads=4):
...
In plain Rust there are no default argument values. Common conventions:
// Option 1: a constant
const DEFAULT_THREADS: usize = 4;
fn ingest(lines: &[String], num_threads: usize) { ... }
// Option 2: at the Python boundary via PyO3
#[pyo3(signature = (lines, num_threads=4))]
fn ingest(...) { ... }
Control Flow
if / else
score = 0.87
if score > 0.9:
print("excellent")
elif score > 0.7:
print("good")
else:
print("needs work")
let score = 0.87_f64;
if score > 0.9 {
println!("excellent");
} else if score > 0.7 {
println!("good");
} else {
println!("needs work");
}
if is an expression — you can use it on the right side of let:
label = "pass" if score > 0.7 else "fail"
let label = if score > 0.7 { "pass" } else { "fail" };
loop
// `loop` = while True
let mut count = 0;
loop {
count += 1;
if count == 3 { break; }
}
// loop can return a value
let result = loop {
count += 1;
if count == 10 { break count * 2; } // break with value
};
while
while queue:
item = queue.pop()
process(item)
while !queue.is_empty() {
let item = queue.pop().unwrap();
process(item);
}
for — Iterating Over Ranges and Collections
for i in range(5):
print(i) # 0 1 2 3 4
for i in range(1, 6):
print(i) # 1 2 3 4 5
for item in my_list:
print(item)
for i, item in enumerate(my_list):
print(i, item)
for i in 0..5 {
println!("{i}"); // 0 1 2 3 4
}
for i in 1..=5 {
println!("{i}"); // 1 2 3 4 5 (..= is inclusive end)
}
for item in &my_vec {
println!("{item}");
}
for (i, item) in my_vec.iter().enumerate() {
println!("{i} {item}");
}
0..5 is a Range value — not just syntax sugar. You can store it, pass it to functions, or chain iterator methods on it.
Pattern Matching
match is like Python's match/case (3.10+), but exhaustive — the compiler forces you to handle every variant.
Basic match
match level:
case LogLevel.INFO:
print("informational")
case LogLevel.WARN:
print("warning")
case LogLevel.ERROR:
print("error")
case _:
print("unknown")
match level {
LogLevel::Info => println!("informational"),
LogLevel::Warn => println!("warning"),
LogLevel::Error => println!("error"),
// No `_` needed if all variants are covered — compiler checks this
}
Exhaustiveness. Remove any arm and the code won't compile. When you add a new enum variant, the compiler points to every match that needs updating.
Matching Enum Variants with Data
match event {
Event::Metric { name, value } => {
println!("metric {name} = {value}");
}
Event::Error { code, message } => {
eprintln!("error {code}: {message}");
}
Event::Heartbeat => {
println!("alive");
}
}
Guards
match score {
s if s > 0.9 => println!("excellent"),
s if s > 0.7 => println!("good"),
_ => println!("needs work"),
}
Multiple Patterns
match day {
"Mon" | "Tue" | "Wed" | "Thu" | "Fri" => println!("weekday"),
"Sat" | "Sun" => println!("weekend"),
_ => println!("unknown"),
}
Ranges in match
match age {
0..=17 => println!("minor"),
18..=64 => println!("adult"),
65.. => println!("senior"),
}
if let — Match One Variant, Ignore the Rest
if isinstance(event, MetricEvent):
print(event.value)
if let Event::Metric { name, value } = &event {
println!("{name} = {value}");
}
while let — Loop Until a Pattern Stops Matching
while let Some(item) = queue.pop() {
process(item);
}
See Also
- Ownership — why you write
&my_vecinstead ofmy_vecinforloops - Structs and Enums — define the enum types that
matchworks on - Error Handling —
OptionandResultwork naturally withmatch