Rust 7 ๐ฆ Functions, Parameters, and Return Values
Functions are the building blocks of Rust programs. Every Rust program has at least one โ main โ and most programs are organized as a collection of functions that call each other. A function packages a sequence of statements behind a name, accepts inputs as parameters, produces an output as a return value, and can be called from anywhere that has access to it. Rust’s function syntax follows the C-family convention with fn, a name, a parameter list, and a return type, but the details are stricter than most languages: parameter types are always required, the return type is explicit unless it is the unit type, and the body is a block that evaluates to a value.
That last point is the most distinctive feature. In Rust, a block is an expression, and the last expression in a function body becomes the return value. There is no need for a return keyword in the common case โ the absence of a semicolon on the final expression is what returns it. This “everything is an expression” model runs through the language and is one of the first things that surprises programmers coming from statement-oriented languages.
This chapter covers function definitions, parameters and their types, return values and the expression-based body, the return keyword and early exits, statements versus expressions, nested functions, the main function, and the conventions that make functions readable.
Key point: Rust functions are declared with fn, parameters always require type annotations, and the return type is written after ->. The body is a block; its final expression is the return value if it has no trailing semicolon. Adding a semicolon turns the expression into a statement and changes the function to return ().
Why functions are structured this way
The explicit-types problem. Rust is statically typed and does not perform type inference across function boundaries. A function’s parameter and return types are part of its public interface, and the compiler checks every call against that interface. This means the caller can type-check a call without reading the function body, and the function can be compiled without knowledge of its callers. Explicit types in signatures are what make separate compilation possible.
The expression problem. In Rust, almost everything is an expression. if, match, blocks, and loops all produce values. A function body is a block, and a block’s value is its final expression. This uniform model means there is no separate “return statement” for the common case โ the value of the block is the value of the function. The return keyword exists for early exits, but it is not the primary mechanism.
The semicolon problem. The difference between an expression and a statement in Rust is the semicolon. 5 is an expression with the value 5. 5; is a statement with the value (). A function whose body ends with 5; returns (), not 5. This is the single most common mistake for newcomers, and it produces a type error that names the expected and found types.
The unit problem. Functions that perform an action rather than computing a value return the unit type (). The return type can be omitted when it is (). This keeps the syntax for side-effecting functions (like println!) clean while making the absence of a value explicit in the type system.
The recursion and forward-reference problem. Rust functions can call each other regardless of declaration order. A function can call another function defined later in the same file, and mutually recursive functions work without forward declarations. The compiler resolves names across the whole module.
a. Defining and calling functions
A function is defined with the fn keyword, a name in snake_case, a parameter list, an optional return type, and a body in braces.
fn greet() {
println!("Hello!");
}
fn main() {
greet();
greet();
}
The name follows snake_case: lowercase words separated by underscores. The compiler warns if a function name is not in snake_case, though it does not error.
Calling a function uses its name followed by parentheses with the arguments:
fn main() {
greet(); // no arguments
}
Functions can be defined in any order. main can call a function defined after it in the file, and functions can call each other without forward declarations.
fn main() {
let result = add(2, 3);
println!("{}", result);
}
fn add(a: i32, b: i32) -> i32 {
a + b
}
b. Parameters and type annotations
Every parameter requires a type annotation. There is no inference for parameters; the function’s signature must be fully explicit.
fn add(a: i32, b: i32) -> i32 {
a + b
}
The parameter name is followed by a colon and the type. Multiple parameters are separated by commas.
fn describe(name: &str, age: u32, active: bool) {
println!("{} is {} and active: {}", name, age, active);
}
A parameter can be shadowed inside the function body, but the original value is not changed for the caller.
fn double(x: i32) -> i32 {
let x = x * 2; // new binding, shadows parameter
x
}
Functions can take references as parameters, which is the standard way to pass values without transferring ownership:
fn length(s: &str) -> usize {
s.len()
}
fn main() {
let name = String::from("Alice");
let n = length(&name); // pass a reference
println!("{}", n); // name is still usable
}
The distinction between String and &str, and between owned values and references, is central to Rust’s ownership model. The convention is to take &str rather than &String for string parameters, and &[T] rather than &Vec<T> for slice parameters, because the reference types are more general and accept more inputs.
// Accepts &str, which includes string literals and &String
fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
greet("Alice"); // &str literal
greet(&String::from("Bob")); // &String coerces to &str
c. Return values and the expression body
A function’s return type is written after ->. The body is a block, and the value of the block is the value of the function.
fn add(a: i32, b: i32) -> i32 {
a + b // no semicolon: this is the return value
}
The final expression must match the declared return type. If it does not, the compiler reports a type mismatch.
fn add(a: i32, b: i32) -> i32 {
a + b; // semicolon: this is a statement; function returns ()
// error: expected i32, found ()
}
This is the semicolon rule. Without a semicolon, the expression is the block’s value. With a semicolon, the expression becomes a statement and the block’s value becomes ().
A function can have multiple statements before the final expression:
fn area(width: u32, height: u32) -> u32 {
let product = width * height; // statement
product // expression: return value
}
The function body evaluates each statement in order, and the final expression’s value is returned.
For functions that return nothing, the return type is () and can be omitted:
fn greet(name: &str) {
println!("Hello, {}!", name);
}
This is equivalent to:
fn greet(name: &str) -> () {
println!("Hello, {}!", name);
}
The explicit form is rarely written; omitting -> () is the convention.
d. The return keyword and early exits
The return keyword exits the function immediately with the given value. It is used for early exits, usually when a condition is not met.
fn divide(a: f64, b: f64) -> f64 {
if b == 0.0 {
return 0.0; // early exit
}
a / b // normal exit
}
A guard clause with return flattens nested conditionals:
fn describe(value: Option<i32>) -> String {
let value = match value {
Some(v) => v,
None => return String::from("none"),
};
format!("value: {}", value)
}
The return is not needed for the final expression, and the Rust style guide prefers the expression form when the return would be at the end of the function:
// Preferred
fn add(a: i32, b: i32) -> i32 {
a + b
}
// Also valid, but discouraged
fn add(a: i32, b: i32) -> i32 {
return a + b;
}
The return is appropriate for early exits, not for the final expression. Using it for both is valid but not idiomatic.
e. Statements versus expressions
The distinction between statements and expressions is fundamental in Rust. An expression produces a value; a statement performs an action. The body of a function is a sequence of statements followed by an optional final expression.
Statements include:
letbindings:let x = 5;- Expression statements:
foo(); - Item declarations:
fn,struct,use
Expressions include:
- Literals:
5,"hello",true - Arithmetic:
a + b - Function calls:
foo() - Blocks:
{ let x = 5; x + 1 } - Control flow that produces values:
if,match, loops
A block is an expression:
let y = {
let x = 3;
x + 1 // block value
};
println!("{}", y); // 4
This is why the function body works the way it does. The body is a block, and the block’s value is the function’s return value.
A common pattern in Rust is to use the final expression of a match or if as the function’s return:
fn sign(n: i32) -> i32 {
if n > 0 {
1
} else if n < 0 {
-1
} else {
0
}
}
The if/else is an expression, and its value is returned. No return keyword and no temporary variable are needed.
f. Nested functions and function items
Rust allows functions to be defined inside other functions. A nested function is a separate item, not a closure, and does not capture variables from the enclosing scope.
fn main() {
fn helper(x: i32) -> i32 {
x * 2
}
println!("{}", helper(5)); // 10
}
Nested functions are useful for organizing a large function’s logic without exposing helpers to the rest of the module. They cannot access local variables of the enclosing function; if they need to, a closure is required.
Functions are first-class values in Rust. A function can be assigned to a variable, passed as an argument, and returned from another function. The type of a function item is written with fn:
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn apply(f: fn(i32, i32) -> i32, a: i32, b: i32) -> i32 {
f(a, b)
}
fn main() {
let result = apply(add, 2, 3);
println!("{}", result); // 5
}
The fn(i32, i32) -> i32 type is a function pointer. Closures have a different type, but they can be passed to functions that accept generic types implementing the Fn traits.
Complete Example Session
// ============================================
// PART 1: BASIC FUNCTION DEFINITION
// ============================================
fn greet() {
println!("Hello!");
}
fn main() {
greet();
}
// ============================================
// PART 2: FUNCTION WITH PARAMETERS
// ============================================
fn greet(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
greet("Alice");
greet("Bob");
}
// ============================================
// PART 3: FUNCTION WITH RETURN VALUE
// ============================================
fn add(a: i32, b: i32) -> i32 {
a + b // no semicolon: this is the return value
}
fn main() {
let result = add(2, 3);
println!("{}", result); // 5
}
// ============================================
// PART 4: THE SEMICOLON RULE
// ============================================
fn add_broken(a: i32, b: i32) -> i32 {
a + b; // semicolon makes this a statement
// function now returns (), type mismatch
}
// Uncomment to see the compile error:
// fn main() {
// let x = add_broken(1, 2);
// }
// ============================================
// PART 5: MULTIPLE STATEMENTS AND FINAL EXPRESSION
// ============================================
fn area(width: u32, height: u32) -> u32 {
let product = width * height;
let adjusted = product + 1;
adjusted
}
fn main() {
println!("{}", area(3, 4)); // 13
}
// ============================================
// PART 6: EARLY RETURN
// ============================================
fn divide(a: f64, b: f64) -> f64 {
if b == 0.0 {
return 0.0;
}
a / b
}
fn main() {
println!("{}", divide(10.0, 2.0)); // 5
println!("{}", divide(10.0, 0.0)); // 0
}
// ============================================
// PART 7: BLOCK AS EXPRESSION
// ============================================
fn main() {
let y = {
let x = 3;
x + 1
};
println!("{}", y); // 4
}
// ============================================
// PART 8: IF/ELSE AS RETURN VALUE
// ============================================
fn sign(n: i32) -> i32 {
if n > 0 {
1
} else if n < 0 {
-1
} else {
0
}
}
fn main() {
println!("{}", sign(42)); // 1
println!("{}", sign(-42)); // -1
println!("{}", sign(0)); // 0
}
// ============================================
// PART 9: NESTED FUNCTION
// ============================================
fn main() {
fn square(x: i32) -> i32 {
x * x
}
let n = 5;
println!("{} squared is {}", n, square(n)); // 25
}
// ============================================
// PART 10: FUNCTION AS PARAMETER
// ============================================
fn add(a: i32, b: i32) -> i32 { a + b }
fn multiply(a: i32, b: i32) -> i32 { a * b }
fn apply(f: fn(i32, i32) -> i32, a: i32, b: i32) -> i32 {
f(a, b)
}
fn main() {
println!("{}", apply(add, 2, 3)); // 5
println!("{}", apply(multiply, 2, 3)); // 6
}
These ten parts cover basic functions, parameters, return values, the semicolon rule, multiple statements, early returns, blocks as expressions, if/else as return values, nested functions, and function pointers as parameters.
Quick Reference
Function Syntax
| Part | Example |
|---|---|
| Declaration | fn name() {} |
| Parameters | fn name(x: i32, y: &str) {} |
| Return type | fn name() -> i32 |
| Body | { ... } |
| Final expression | Last expression without semicolon |
| Early return | return value; |
| Unit return | fn name() {} or fn name() -> () |
Parameter Types
| Type | Purpose |
|---|---|
i32, f64, bool | Copy types, passed by value |
&str | String slice reference |
&[T] | Slice reference |
&mut T | Mutable reference |
T | Owned value, transferred |
impl Trait | Generic parameter with trait bound |
Return Value Rules
| Form | Result |
|---|---|
a + b | Returns a + b |
a + b; | Returns () |
return a + b; | Returns a + b early |
if x { 1 } else { 2 } | Returns 1 or 2 |
match x { ... } | Returns match value |
Statements vs Expressions
| Kind | Examples | Produces Value |
|---|---|---|
| Statement | let x = 5;, foo(); | No |
| Expression | 5, a + b, { ... }, if ... | Yes |
Best Practices
โ Do This:
fn add(a: i32, b: i32) -> i32 { // Final expression, no return
a + b
}
fn greet(name: &str) { // Omit -> () for side effects
println!("Hello, {}!", name);
}
fn find(items: &[i32], target: i32) -> Option<usize> {
for (i, &v) in items.iter().enumerate() {
if v == target {
return Some(i); // Early return for found case
}
}
None // Final expression
}
fn describe(name: &str) -> String { // Take &str, not &String
format!("User: {}", name)
}
โ Don’t Do This:
fn add(a: i32, b: i32) -> i32 {
a + b; // โ Semicolon changes return to ()
}
fn add(a: i32, b: i32) -> i32 {
return a + b; // โ return at end is unnecessary
}
fn greet(name: String) { // โ Owns string unnecessarily
println!("{}", name);
}
fn process() -> i32 {
let x = 5; // โ Missing final expression
}
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
Return type mismatch () vs T | Semicolon on final expression | Remove the semicolon |
Unused return warning | return at function end | Use expression form |
| Parameter type missing | Rust requires explicit types | Add : Type to parameter |
| Ownership error on parameter | Passing owned String | Pass &str instead |
| Nested function cannot capture | Function items do not capture | Use a closure |
| Function item not a pointer | Different types | Use fn type for pointers |
Real-World Examples
1. Simple Calculator
fn add(a: f64, b: f64) -> f64 { a + b }
fn subtract(a: f64, b: f64) -> f64 { a - b }
fn multiply(a: f64, b: f64) -> f64 { a * b }
2. Guard Clause
fn process(input: &str) -> Result<usize, String> {
if input.is_empty() {
return Err(String::from("empty input"));
}
Ok(input.len())
}
3. Multiple Return Values
fn min_max(values: &[i32]) -> (i32, i32) {
let mut min = values[0];
let mut max = values[0];
for &v in values {
if v < min { min = v; }
if v > max { max = v; }
}
(min, max)
}
4. Early Return in Loop
fn find_first_even(values: &[i32]) -> Option<i32> {
for &v in values {
if v % 2 == 0 {
return Some(v);
}
}
None
}
5. If/Else as Return
fn grade(score: u32) -> char {
if score >= 90 { 'A' }
else if score >= 80 { 'B' }
else if score >= 70 { 'C' }
else { 'F' }
}
6. Match as Return
fn describe(n: i32) -> &'static str {
match n {
0 => "zero",
n if n > 0 => "positive",
_ => "negative",
}
}
7. Function Pointer
fn apply(f: fn(i32) -> i32, x: i32) -> i32 {
f(x)
}
8. Nested Helper
fn calculate(x: i32) -> i32 {
fn square(n: i32) -> i32 { n * n }
square(x) + x
}
9. Unit Return
fn log(message: &str) {
println!("[LOG] {}", message);
}
10. Recursive Function
fn factorial(n: u32) -> u32 {
if n <= 1 {
1
} else {
n * factorial(n - 1)
}
}
Visual
Function Anatomy
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ fn add (a: i32, b: i32) -> i32 { โ
โ โ โ โ โ โ โ
โ โ โ โ โ โโโ Body block โ
โ โ โ โ โโโ Return type โ
โ โ โ โโโ Parameters with types โ
โ โ โโโ Name in snake_case โ
โ โโโ Keyword โ
โ โ
โ a + b โ
โ โโโ Final expression (no semicolon = return value) โ
โ } โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Semicolon Rule
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ fn add(a: i32, b: i32) -> i32 { โ
โ a + b โ expression; value is i32 โ
โ } โ returns i32 โ
โ
โ โ
โ fn add(a: i32, b: i32) -> i32 { โ
โ a + b; โ statement; value is () โ
โ } โ returns () โ type mismatch โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Statements vs Expressions
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ STATEMENTS (no value) EXPRESSIONS (produce value) โ
โ โ
โ let x = 5; x + 5 โ
โ foo(); 5 โ
โ fn f() {} { let y = 3; y + 1 } โ
โ if x > 0 { 1 } else { 0 } โ
โ match x { ... } โ
โ โ
โ Function bodies are blocks. The final expression's value โ
โ becomes the return value. A semicolon makes it a statement โ
โ and the return value becomes (). โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Return Paths
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ fn find(items: &[i32], target: i32) -> Option<usize> { โ
โ โ
โ for (i, &v) in items.iter().enumerate() { โ
โ if v == target { โ
โ return Some(i); โโโ early exit โโโถ โ
โ } โ
โ } โ
โ โ
โ None โโโ normal exit โโโถ โ
โ } โ
โ โ
โ Both paths return Option<usize>. The compiler verifies โ
โ that all paths return the declared type. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| Declaration | fn name(params) -> ReturnType { body } |
| Name convention | snake_case |
| Parameter types | Always required |
| Return type | -> T; omitted for () |
| Final expression | Return value if no semicolon |
| Semicolon on final | Changes return to () |
| Early return | return value; |
| Unit type | () for functions with no return |
| Nested function | fn inside fn, no capture |
| Function pointer | fn(i32) -> i32 |
| Recursion | Supported; no forward declaration needed |
Key takeaways:
- Parameter types are always required. Rust does not infer types across function boundaries; the signature must be fully explicit.
- The return type follows
->. It can be omitted only when the function returns(). The compiler checks that every path returns the declared type. - A function body is a block, and the block’s value is the return. The final expression, without a semicolon, is the return value. This is the idiomatic form for simple returns.
- The semicolon rule is the most common mistake. Adding a semicolon to the final expression turns it into a statement and changes the return type to
(). The error message names the mismatch. returnis for early exits. It is not needed at the end of a function and is discouraged there. Use it when a condition causes the function to exit before reaching the final expression.- Blocks are expressions.
let y = { let x = 3; x + 1 };is valid, and the block’s value is4. This is whyif/elseandmatchcan be the final expression of a function. - Functions can be nested. A nested function is a separate item and does not capture the enclosing scope. For capture, use a closure.
- Functions are values. A function can be assigned to a variable, passed as an argument, and returned from another function. The
fntype describes a function pointer.
Remember: Functions in Rust are explicit about their interface. Parameters are typed, the return type is declared, and the body is a block whose value is the result. The most distinctive feature is the expression-based body: the last expression without a semicolon is the return value, and adding a semicolon changes the return type to (). This is not a quirk; it is a consequence of Rust’s uniform expression model, where blocks, if, and match all produce values. The return keyword exists for early exits, and guard clauses use it to flatten nesting. Functions can be nested, passed as arguments, and returned from other functions. Once you internalize the semicolon rule, function bodies become natural to read: a sequence of statements followed by an expression that is the result.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!