Rust Functions: From Basics to Advanced
Functions in Rust are reusable blocks of code that perform specific tasks. They are declared with the fn keyword, can accept parameters, return values, and enforce strict type checking at compile time. Rust functions support expression-based returns, generics, and advanced features like closures and higher-order functions.
1. Defining and Calling Functions
Basic function syntax in Rust requires explicit type annotations for parameters and return values. Functions are defined using the fn keyword and called by their name followed by parentheses.
// Basic function definition
fn greet(name: &str) { // &str parameter type
println!("Hello, {}!", name); // No return value
}
// Function with return value
fn add(a: i32, b: i32) -> i32 { // -> specifies return type
a + b // Expression without semicolon = return value
}
// Calling functions
fn main() {
greet("Alice"); // Call with string literal
let sum = add(5, 3); // Capture return value
println!("Sum: {}", sum);
}
Function Quiz
Why doesn't the add function use a return keyword?
Key Points: Rust functions use snake_case naming. The return type must be specified after ->. Expressions without semicolons are returned implicitly, while return is used for early returns.
2. Parameters and Arguments
Function parameters in Rust can be immutable or mutable references, and the compiler enforces proper borrowing rules. Parameters must always have explicit types.
// Immutable parameter (borrows the String)
fn print_length(s: &String) {
println!("Length: {}", s.len()); // Can read but not modify
}
// Mutable parameter (borrows mutably)
fn append_world(s: &mut String) {
s.push_str(" world!"); // Can modify the string
}
// Multiple parameters with different types
fn calculate(x: f64, y: f64, operation: char) -> f64 {
match operation {
'+' => x + y, // Pattern matching on operation
'-' => x - y,
_ => panic!("Unknown operation"), // Crash on invalid input
}
}
Parameters Quiz
What's the difference between &String and &mut String parameters?
Parameter Rules: Rust enforces one mutable reference OR multiple immutable references. Function signatures document whether parameters are borrowed or owned. Complex types should generally be passed by reference.
3. Returning Values from Functions
Return values in Rust can be explicit using the return keyword or implicit via expressions. Functions can return multiple values using tuples, or optional values using Option/Result.
// Early return with explicit return statement
fn early_return(x: i32) -> i32 {
if x < 0 {
return 0; // Early exit with return
}
x * 2 // Implicit return (no semicolon)
}
// Returning multiple values with a tuple
fn stats(numbers: &[i32]) -> (f64, i32) {
let sum: i32 = numbers.iter().sum();
let mean = sum as f64 / numbers.len() as f64;
(mean, sum) // Return tuple (parentheses required)
}
// Returning Option for fallible operations
fn divide(a: f64, b: f64) -> Option<f64> {
if b == 0.0 {
None // Indicate failure
} else {
Some(a / b) // Wrap success value
}
}
Return Quiz
What does this function return if numbers = [1, 2, 3]?
fn mystery(numbers: &[i32]) -> i32 {
numbers.iter().fold(0, |acc, &x| acc + x)
}
Return Best Practices: Prefer implicit returns for simple functions. Use Option when operations might fail. Return tuples for multiple values, but consider structs for complex cases.
4. Function Pointers and Higher-Order Functions
Function pointers allow treating functions as values. Higher-order functions accept or return other functions, enabling powerful abstraction patterns.
// Type alias for function pointer
type MathOp = fn(i32, i32) -> i32;
// Higher-order function accepting a function pointer
fn apply_operation(a: i32, b: i32, op: MathOp) -> i32 {
op(a, b) // Call the passed function
}
// Concrete operation functions
fn add(x: i32, y: i32) -> i32 { x + y }
fn multiply(x: i32, y: i32) -> i32 { x * y }
fn main() {
// Pass add function as argument
let result = apply_operation(3, 4, add);
println!("Result: {}", result); // 7
// Pass multiply function
let product = apply_operation(3, 4, multiply);
println!("Product: {}", product); // 12
}
Function Pointers Quiz
What is the type of add in this code?
Function Pointer Notes: Function pointers differ from closures (which capture environment). The type syntax explicitly shows parameter and return types. Useful for strategy patterns or callbacks.
5. Generic Functions
Generic functions work with multiple types while maintaining compile-time type safety. Type parameters are declared in angle brackets and can have trait bounds.
// Generic function with trait bound
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest { // Requires PartialOrd
largest = item;
}
}
largest
}
// Multiple generic type parameters
fn pair<T, U>(first: T, second: U) -> (T, U) {
(first, second) // Return tuple of generic types
}
fn main() {
let numbers = vec![34, 50, 25, 100, 65];
println!("Largest: {}", largest(&numbers));
let mixed = pair("hello", 42); // Different types
println!("Pair: {:?}", mixed);
}
Generics Quiz
What does T: PartialOrd mean in the generic constraint?
Generic Tips: Use trait bounds to specify required functionality. Compiler generates specialized versions for each used type. Generics enable code reuse without runtime costs.