Functions
Functions are the building blocks of Rust code. We've already seen main(), but Rust has rich function capabilities.
Function Basics
Defining Functions
fn main() {
println!("Hello from main!");
another_function();
}
fn another_function() {
println!("Hello from another function!");
}
Python comparison:
def main():
print("Hello from main!")
another_function()
def another_function():
print("Hello from another function!")
if __name__ == "__main__":
main()
Key differences:
- Rust:
fnkeyword, type annotations required - Python:
defkeyword, type hints optional - Rust: Use
snake_casefor function names (same as Python)
Parameters
fn main() {
print_labeled_measurement(5, 'h');
}
fn print_labeled_measurement(value: i32, unit_label: char) {
println!("The measurement is: {}{}", value, unit_label);
}
Python comparison:
def print_labeled_measurement(value: int, unit_label: str):
print(f"The measurement is: {value}{unit_label}")
print_labeled_measurement(5, 'h')
Type annotations are mandatory in Rust (optional in Python).
Return Values
fn five() -> i32 {
5 // No semicolon - this is an expression
}
fn plus_one(x: i32) -> i32 {
x + 1 // Expression returns value
}
fn main() {
let x = five();
println!("The value of x is: {}", x);
let y = plus_one(5);
println!("The value of y is: {}", y);
}
Python comparison:
def five() -> int:
return 5 # Explicit return required
def plus_one(x: int) -> int:
return x + 1
x = five()
y = plus_one(5)
Key difference: In Rust, the last expression is automatically returned (no return keyword needed). Python always requires explicit return.
Explicit Return
fn add(a: i32, b: i32) -> i32 {
return a + b; // Early return with return keyword
}
fn max(a: i32, b: i32) -> i32 {
if a > b {
return a; // Early return
}
b // Final expression
}
Statements vs Expressions
Statements
Don't return values:
fn main() {
let y = 6; // Statement
// let x = (let y = 6); // ERROR: statements don't return values
}
Expressions
Return values:
fn main() {
let y = {
let x = 3;
x + 1 // Expression (no semicolon)
};
println!("The value of y is: {}", y); // 4
}
Key Difference: Expressions don't end with semicolons.
fn main() {
let x = 5;
let y = {
let x = 3;
x + 1 // Expression - returns 4
};
let z = {
let x = 3;
x + 1; // Statement (semicolon) - returns ()
};
println!("y = {}, z = {:?}", y, z); // y = 4, z = ()
}
Multiple Return Values
Use tuples:
fn calculate(x: i32, y: i32) -> (i32, i32, i32) {
(x + y, x - y, x * y)
}
fn main() {
let (sum, diff, product) = calculate(10, 5);
println!("sum: {}, diff: {}, product: {}", sum, diff, product);
}
Method Syntax
Methods are functions defined within a struct, enum, or trait context.
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// Method with &self
fn area(&self) -> u32 {
self.width * self.height
}
// Method with &mut self
fn expand(&mut self, amount: u32) {
self.width += amount;
self.height += amount;
}
// Associated function (no self)
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}
fn main() {
let rect = Rectangle { width: 30, height: 50 };
println!("Area: {}", rect.area());
let mut rect2 = Rectangle { width: 10, height: 10 };
rect2.expand(5);
println!("Width: {}", rect2.width);
let sq = Rectangle::square(20);
println!("Square area: {}", sq.area());
}
self, &self, &mut self
| Syntax | Ownership | Use Case |
|---|---|---|
self | Takes ownership | Consuming the value |
&self | Borrows immutably | Read-only access |
&mut self | Borrows mutably | Modifying the value |
Closures
Anonymous functions that can capture their environment.
Basic Closure
fn main() {
let add_one = |x| x + 1;
println!("{}", add_one(5)); // 6
}
Closure Syntax
fn main() {
// Minimal
let add = |x, y| x + y;
// With types
let add: fn(i32, i32) -> i32 = |x, y| x + y;
// With body block
let add = |x, y| {
let result = x + y;
result
};
println!("{}", add(2, 3));
}
Capturing Environment
fn main() {
let x = 4;
let equal_to_x = |z| z == x; // Captures x
let y = 4;
println!("{}", equal_to_x(y)); // true
}
Capture Modes
| Mode | Syntax | Description |
|---|---|---|
| Borrow | Default | |x| x + env |
| Mutable borrow | Default if needed | |x| { env += x; } |
| Move | move keyword | move |x| x + env |
fn main() {
let list = vec![1, 2, 3];
// Borrows list
let only_borrows = || println!("List: {:?}", list);
only_borrows();
println!("List still valid: {:?}", list);
// Takes ownership
let move_closure = move || println!("List: {:?}", list);
move_closure();
// println!("{:?}", list); // ERROR: list was moved
}
Using Closures with Iterators
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// Map
let doubled: Vec<i32> = numbers.iter()
.map(|x| x * 2)
.collect();
// Filter
let even: Vec<i32> = numbers.iter()
.filter(|&x| x % 2 == 0)
.copied()
.collect();
// Find
let first_large = numbers.iter()
.find(|&&x| x > 3);
println!("Doubled: {:?}", doubled);
println!("Even: {:?}", even);
println!("First large: {:?}", first_large);
}
Function Pointers
Functions can be passed as arguments:
fn add_one(x: i32) -> i32 {
x + 1
}
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg)
}
fn main() {
let result = do_twice(add_one, 5);
println!("Result: {}", result); // 12
}
Higher-Order Functions
Functions that take or return functions:
fn apply_operation(x: i32, y: i32, op: fn(i32, i32) -> i32) -> i32 {
op(x, y)
}
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn multiply(a: i32, b: i32) -> i32 {
a * b
}
fn main() {
println!("10 + 5 = {}", apply_operation(10, 5, add));
println!("10 * 5 = {}", apply_operation(10, 5, multiply));
}
Common Patterns
Builder Pattern
struct Config {
host: String,
port: u16,
timeout: u64,
}
impl Config {
fn new() -> Self {
Config {
host: "localhost".to_string(),
port: 8080,
timeout: 30,
}
}
fn host(mut self, host: &str) -> Self {
self.host = host.to_string();
self
}
fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
fn build(self) -> Self {
self
}
}
fn main() {
let config = Config::new()
.host("example.com")
.port(3000)
.build();
println!("{}:{}", config.host, config.port);
}
Option Returning Functions
fn divide(dividend: f64, divisor: f64) -> Option<f64> {
if divisor == 0.0 {
None
} else {
Some(dividend / divisor)
}
}
fn main() {
match divide(10.0, 2.0) {
Some(result) => println!("Result: {}", result),
None => println!("Cannot divide by zero"),
}
}
Combinator Pattern
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let result: i32 = numbers
.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * x)
.sum();
println!("Sum of squares of evens: {}", result); // 20
}
Practice Exercises
Exercise 1: Temperature Conversion
Create functions to convert between Celsius and Fahrenheit:
fn celsius_to_fahrenheit(c: f64) -> f64 {
// Your code
}
fn fahrenheit_to_celsius(f: f64) -> f64 {
// Your code
}
Exercise 2: Fibonacci
Write a function that returns the nth Fibonacci number:
fn fibonacci(n: u32) -> u32 {
// Your code
}
Exercise 3: Filter and Transform
Use closures to:
- Filter a list of numbers to only evens
- Square each number
- Sum the results
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Your code
}
Exercise 4: Calculator
Create a calculator that takes an operation as a function pointer:
fn calculate(a: i32, b: i32, op: fn(i32, i32) -> i32) -> i32 {
op(a, b)
}
// Define add, subtract, multiply, divide functions
Key Takeaways
- Functions use
snake_casenaming - Parameters must have type annotations
- Return types are specified with
-> - Expressions return values, statements don't
- Methods are functions in impl blocks
- Closures are anonymous functions that capture environment
- Function pointers allow passing functions as arguments
- Use iterators with closures for powerful data transformations
Next Steps
In the next chapter, we'll explore structs and enums - Rust's way of creating custom data types.
Quick Reference
// Function
fn add(x: i32, y: i32) -> i32 {
x + y
}
// Method
impl MyStruct {
fn method(&self) {}
}
// Closure
let add = |x, y| x + y;
// Closure with move
let closure = move || { /* uses captured variables */ };
// Function pointer
fn do_twice(f: fn(i32) -> i32, x: i32) -> i32 {
f(x) + f(x)
}
// Iterator with closures
vec.iter()
.filter(|&x| x > 0)
.map(|x| x * 2)
.collect()