Functions
Functions are fundamental building blocks in Rust. They allow you to organize code into reusable pieces and create clear abstractions. Rust functions follow specific rules about parameters, return values, and the distinction between statements and expressions.
Function Basics
Functions in Rust are declared using the fn
keyword:
fn main() {
println!("Hello, world!");
greet();
}
fn greet() {
println!("Hello from the greet function!");
}
Function Naming Convention
Rust uses snake_case for function names:
fn calculate_area() { }
fn get_user_name() { }
fn is_valid_email() { }
// Not recommended:
// fn calculateArea() { } // camelCase
// fn GetUserName() { } // PascalCase
Function Parameters
Functions can take parameters of specified types:
fn main() {
greet_person("Alice");
let result = add(5, 3);
println!("5 + 3 = {}", result);
}
fn greet_person(name: &str) {
println!("Hello, {}!", name);
}
fn add(x: i32, y: i32) -> i32 {
x + y
}
Multiple Parameters
fn describe_person(name: &str, age: u32, is_student: bool) {
println!("{} is {} years old", name, age);
if is_student {
println!("{} is a student", name);
} else {
println!("{} is not a student", name);
}
}
fn main() {
describe_person("Bob", 25, false);
}
Parameter Patterns
You can destructure parameters:
fn print_coordinates((x, y): (i32, i32)) {
println!("Point is at ({}, {})", x, y);
}
fn print_name_and_age(person: (&str, u32)) {
let (name, age) = person;
println!("{} is {} years old", name, age);
}
fn main() {
print_coordinates((10, 20));
print_name_and_age(("Charlie", 30));
}
Return Values
Functions can return values using the ->
syntax:
fn square(x: i32) -> i32 {
x * x // Expression without semicolon
}
fn is_even(x: i32) -> bool {
x % 2 == 0
}
fn get_greeting() -> String {
String::from("Hello, Rust!")
}
fn main() {
let num = 4;
println!("{} squared is {}", num, square(num));
println!("{} is even: {}", num, is_even(num));
println!("{}", get_greeting());
}
Early Return
Use the return
keyword for early returns:
fn divide(x: f64, y: f64) -> Option<f64> {
if y == 0.0 {
return None; // Early return
}
Some(x / y)
}
fn check_password(password: &str) -> Result<(), &str> {
if password.len() < 8 {
return Err("Password too short");
}
if !password.chars().any(|c| c.is_numeric()) {
return Err("Password must contain a number");
}
Ok(())
}
Statements vs Expressions
Understanding the difference between statements and expressions is crucial:
Statements
Statements perform actions but don't return values:
fn main() {
let x = 5; // Statement
let y = 10; // Statement
let z = add(x, y); // Statement (assignment)
}
fn add(a: i32, b: i32) -> i32 {
let sum = a + b; // Statement
sum // Expression (returned)
}
Expressions
Expressions evaluate to values:
fn main() {
let x = 5;
// Block expression
let y = {
let inner = 3;
inner + 1 // Expression: evaluates to 4
};
// if expression
let max = if x > y { x } else { y };
println!("y: {}, max: {}", y, max);
}
Common Expression Pitfall
Adding a semicolon turns an expression into a statement:
fn returns_value() -> i32 {
5 // Expression: returns 5
}
fn returns_unit() -> () {
5; // Statement: returns () (unit type)
}
// This would cause a compile error:
// fn wrong() -> i32 {
// 5; // Error: expected i32, found ()
// }
Function Ownership and Borrowing
Functions follow Rust's ownership rules:
Taking Ownership
fn take_ownership(s: String) {
println!("{}", s);
} // s goes out of scope and is dropped
fn main() {
let s = String::from("hello");
take_ownership(s);
// println!("{}", s); // Error: s has been moved
}
Borrowing with References
fn calculate_length(s: &String) -> usize {
s.len()
} // s goes out of scope, but the data it refers to is not dropped
fn main() {
let s = String::from("hello");
let len = calculate_length(&s);
println!("Length of '{}' is {}", s, len); // s is still valid
}
Mutable References
fn append_world(s: &mut String) {
s.push_str(", world!");
}
fn main() {
let mut s = String::from("hello");
append_world(&mut s);
println!("{}", s); // "hello, world!"
}
Advanced Function Concepts
Function Pointers
Functions can be stored in variables and passed as arguments:
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn multiply(a: i32, b: i32) -> i32 {
a * b
}
fn apply_operation(x: i32, y: i32, op: fn(i32, i32) -> i32) -> i32 {
op(x, y)
}
fn main() {
let result1 = apply_operation(5, 3, add);
let result2 = apply_operation(5, 3, multiply);
println!("5 + 3 = {}", result1);
println!("5 * 3 = {}", result2);
// Store function in variable
let operation: fn(i32, i32) -> i32 = add;
println!("Using stored function: {}", operation(10, 20));
}
Higher-Order Functions
Functions that take other functions as parameters:
fn apply_to_each<F>(vec: &mut Vec<i32>, func: F)
where
F: Fn(i32) -> i32,
{
for item in vec.iter_mut() {
*item = func(*item);
}
}
fn double(x: i32) -> i32 {
x * 2
}
fn main() {
let mut numbers = vec![1, 2, 3, 4, 5];
apply_to_each(&mut numbers, double);
println!("{:?}", numbers); // [2, 4, 6, 8, 10]
}
Closures
Closures are anonymous functions that can capture their environment:
fn main() {
let x = 4;
// Closure that captures x
let equal_to_x = |z| z == x;
let y = 4;
assert!(equal_to_x(y));
// Closure with explicit types
let add_one = |x: i32| -> i32 { x + 1 };
println!("5 + 1 = {}", add_one(5));
// Multi-line closure
let expensive_closure = |num| {
println!("Calculating slowly...");
std::thread::sleep(std::time::Duration::from_secs(1));
num
};
println!("Result: {}", expensive_closure(42));
}
Closure Capture Modes
fn main() {
let x = vec![1, 2, 3];
// By reference (Fn)
let print_x = || println!("x: {:?}", x);
print_x();
println!("x is still available: {:?}", x);
// By mutable reference (FnMut)
let mut y = vec![1, 2, 3];
let mut modify_y = || y.push(4);
modify_y();
println!("y after modification: {:?}", y);
// By value (FnOnce)
let z = vec![1, 2, 3];
let consume_z = move || {
println!("z: {:?}", z);
z // z is moved into the closure
};
let result = consume_z();
// println!("{:?}", z); // Error: z has been moved
}
Generic Functions
Functions can be generic over types:
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {}", result);
}
Error Handling in Functions
Using Result Type
use std::num::ParseIntError;
fn parse_number(s: &str) -> Result<i32, ParseIntError> {
s.parse()
}
fn add_numbers(a: &str, b: &str) -> Result<i32, ParseIntError> {
let num_a = parse_number(a)?;
let num_b = parse_number(b)?;
Ok(num_a + num_b)
}
fn main() {
match add_numbers("10", "20") {
Ok(result) => println!("Sum: {}", result),
Err(e) => println!("Error: {}", e),
}
}
Using Option Type
fn find_first_word(s: &str) -> Option<&str> {
s.split_whitespace().next()
}
fn get_file_extension(filename: &str) -> Option<&str> {
filename.rfind('.').map(|i| &filename[i + 1..])
}
fn main() {
if let Some(word) = find_first_word("hello world") {
println!("First word: {}", word);
}
match get_file_extension("document.pdf") {
Some(ext) => println!("Extension: {}", ext),
None => println!("No extension found"),
}
}
Function Documentation
Document your functions using doc comments:
/// Calculates the area of a rectangle.
///
/// # Arguments
///
/// * `width` - The width of the rectangle
/// * `height` - The height of the rectangle
///
/// # Examples
///
/// ```
/// let area = calculate_area(10.0, 5.0);
/// assert_eq!(area, 50.0);
/// ```
fn calculate_area(width: f64, height: f64) -> f64 {
width * height
}
/// Divides two numbers, returning None if the divisor is zero.
///
/// # Examples
///
/// ```
/// assert_eq!(safe_divide(10.0, 2.0), Some(5.0));
/// assert_eq!(safe_divide(10.0, 0.0), None);
/// ```
fn safe_divide(dividend: f64, divisor: f64) -> Option<f64> {
if divisor == 0.0 {
None
} else {
Some(dividend / divisor)
}
}
Best Practices
1. Keep Functions Small and Focused
// Good: Single responsibility
fn validate_email(email: &str) -> bool {
email.contains('@') && email.contains('.')
}
fn send_welcome_email(email: &str) -> Result<(), String> {
if !validate_email(email) {
return Err("Invalid email".to_string());
}
// Send email logic
Ok(())
}
2. Use Descriptive Names
// Good
fn calculate_monthly_payment(principal: f64, rate: f64, months: u32) -> f64 {
// Implementation
0.0
}
// Avoid
fn calc(p: f64, r: f64, m: u32) -> f64 {
0.0
}
3. Prefer Returning Values over Mutation
// Good: Functional style
fn add_prefix(s: String, prefix: &str) -> String {
format!("{}{}", prefix, s)
}
// Less ideal: Mutation
fn add_prefix_mut(s: &mut String, prefix: &str) {
s.insert_str(0, prefix);
}
4. Use Type Annotations When Helpful
// Clear parameter types
fn process_data(data: &[u8], format: &str) -> Result<String, Box<dyn std::error::Error>> {
// Implementation
Ok(String::new())
}
Functions in Rust are powerful tools for creating clean, reusable, and safe code. Understanding ownership, borrowing, expressions vs statements, and advanced concepts like closures and generics will help you write effective Rust programs.