Functions
Define reusable logic with fn, declare parameter types, return values, and understand the expression-vs-statement distinction that makes Rust functions tick.
- Define a function with fn, typed parameters, and a return type
- Return a value using an expression at the end of a function body
- Explain the difference between an expression and a statement in Rust
- Pass values to functions and use the result
The fundamentals track describes functions as the unit of decomposition — named, reusable pieces of logic. Rust's syntax for them is compact, and there's one nuance that trips up almost everyone coming from another language: expressions vs statements. Understanding this once unlocks a lot of idiomatic Rust.
Defining a function
fn add(a: i32, b: i32) -> i32 {
a + b
}Breaking it down:
fnintroduces a function definition.addis the name.(a: i32, b: i32)are the parameters — in Rust, every parameter must have an explicit type. The compiler won't infer them.-> i32is the return type.- The body is
a + bwith no semicolon.
Call it like any other function:
fn main() {
let result = add(2, 3);
println!("2 + 3 = {}", result); // 2 + 3 = 5
}Expressions vs statements
This is Rust's most important syntactic idea at the beginner level.
A statement performs an action and returns nothing useful. let x = 5; is a statement — the semicolon ends it.
An expression evaluates to a value. 2 + 3 is an expression. add(2, 3) is an expression. An if block is an expression. A block { ... } is an expression if its last line has no semicolon.
In the add function above, the last line is a + b — no semicolon. That makes it an expression, and its value becomes the function's return value. If you accidentally add a semicolon:
fn add(a: i32, b: i32) -> i32 {
a + b; // now a statement — returns ()
}error[E0308]: mismatched types
--> src/main.rs:2:24
|
1 | fn add(a: i32, b: i32) -> i32 {
| ^^^ expected `i32`, found `()`The compiler expected an i32 but the function returned () (the unit type — nothing). Remove the semicolon.
The missing-semicolon return is idiomatic Rust, not a typo. When you see a function body whose last line has no semicolon, that line is the return value. You can also use return explicitly (return a + b;), but most Rust code uses the implicit form for the final value.
Using return explicitly
return is available and required for early returns:
fn first_positive(values: &[i32]) -> i32 {
for v in values {
if *v > 0 {
return *v; // early exit
}
}
-1 // default — no semicolon, implicit return
}Early returns express "we're done, here's the answer" without nesting the rest of the function inside an else.
Functions as documentation
Well-named functions with explicit parameter types are self-documenting. Compare:
// Harder to follow
let r = compute(x, 3600);
// Clear intent
let r = convert_seconds_to_hours(x, 3600);In Rust, the type signature alone often tells you what a function does and what it won't accept. This is part of the Rust philosophy: make illegal states unrepresentable.
Check your understanding
Knowledge check
- 1.In Rust, what makes the last line of a function its return value?
- 2.Rust can infer parameter types in function definitions, so you do not need to annotate them.
- 3.What does Rust return from a function whose last statement ends with a semicolon, when the declared return type is ()?
Do it yourself
Add this to your project's src/main.rs and run cargo run:
fn celsius_to_fahrenheit(c: f64) -> f64 {
c * 9.0 / 5.0 + 32.0
}
fn main() {
let boiling = celsius_to_fahrenheit(100.0);
let freezing = celsius_to_fahrenheit(0.0);
println!("100°C = {}°F", boiling);
println!("0°C = {}°F", freezing);
}Then try adding a semicolon after the expression in celsius_to_fahrenheit and see what the compiler says.
Where to go next
Functions are how you structure logic. Next: control flow — making decisions with if/else, repeating work with loop, while, and for, and Rust's range syntax.