Introduction to Functions in Rust
Functions are the fundamental building blocks of Rust programs. They encapsulate behavior, promote code reuse, and help organize code into logical, manageable pieces. Rust's function system combines elements from functional and systems programming, offering powerful features like pattern matching in parameters, multiple return values, and function pointers.
Key Concepts
- Function Declaration: Using the
fnkeyword - Parameters: Typed inputs to functions
- Return Values: Explicit or implicit returns
- Statements vs Expressions: Understanding the difference
- Function Types: First-class functions and closures
- Safety: Borrowing rules apply to function boundaries
1. Basic Function Syntax
Simple Functions
// Basic function with no parameters and no return
fn greet() {
println!("Hello, world!");
}
// Function with parameters
fn greet_person(name: &str) {
println!("Hello, {}!", name);
}
// Function with return value
fn add(a: i32, b: i32) -> i32 {
a + b // Implicit return (no semicolon)
}
// Function with explicit return
fn multiply(a: i32, b: i32) -> i32 {
return a * b; // Explicit return (with semicolon)
}
fn main() {
greet();
greet_person("Alice");
let sum = add(5, 3);
println!("Sum: {}", sum);
let product = multiply(4, 7);
println!("Product: {}", product);
}
Function Ordering
// Functions can be defined after they're called
fn main() {
let result = calculate(10, 5);
println!("Result: {}", result);
call_before_definition(); // Works due to hoisting
}
// Definition after main
fn calculate(x: i32, y: i32) -> i32 {
x * y + y
}
fn call_before_definition() {
println!("This works because functions are hoisted");
}
// Nested functions (allowed)
fn outer_function() {
println!("Outer function");
fn inner_function() {
println!("Inner function");
}
inner_function(); // Can only be called within outer_function
}
2. Parameters and Arguments
Parameter Patterns
// Basic parameters
fn basic_params(x: i32, y: f64, z: &str) {
println!("x: {}, y: {}, z: {}", x, y, z);
}
// Pattern matching in parameters
fn print_coordinates(&(x, y): &(i32, i32)) {
println!("Coordinates: ({}, {})", x, y);
}
// Destructuring structs
struct Point {
x: i32,
y: i32,
}
fn print_point(Point { x, y }: &Point) {
println!("Point at ({}, {})", x, y);
}
// Multiple patterns
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
}
fn handle_message(msg: Message) {
match msg {
Message::Quit => println!("Quit"),
Message::Move { x, y } => println!("Move to ({}, {})", x, y),
Message::Write(text) => println!("Write: {}", text),
}
}
fn main() {
basic_params(10, 3.14, "hello");
let tuple = (5, 10);
print_coordinates(&tuple);
let point = Point { x: 15, y: 25 };
print_point(&point);
handle_message(Message::Move { x: 10, y: 20 });
}
Reference Parameters
// Immutable reference
fn calculate_length(s: &String) -> usize {
s.len()
}
// Mutable reference
fn append_world(s: &mut String) {
s.push_str(", world");
}
// Multiple references
fn process_strings(s1: &String, s2: &String) -> String {
format!("{} {}", s1, s2)
}
// Slice parameters
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let s = String::from("hello");
let len = calculate_length(&s);
println!("Length: {}", len);
let mut s = String::from("hello");
append_world(&mut s);
println!("Modified: {}", s);
let s1 = String::from("hello");
let s2 = String::from("world");
let result = process_strings(&s1, &s2);
println!("Combined: {}", result);
let sentence = String::from("hello world");
let word = first_word(&sentence);
println!("First word: {}", word);
}
3. Return Values
Different Return Patterns
// Single return value
fn square(x: i32) -> i32 {
x * x
}
// Multiple return values via tuple
fn divide(dividend: i32, divisor: i32) -> (i32, i32) {
(dividend / divisor, dividend % divisor)
}
// Named return values (not like Go, just for documentation)
fn get_coordinates() -> (x: i32, y: i32) {
(10, 20) // Return tuple, names are for documentation only
}
// Returning references
fn get_first_element(vec: &[i32]) -> Option<&i32> {
vec.first()
}
// Returning Result
fn safe_divide(numerator: f64, denominator: f64) -> Result<f64, String> {
if denominator == 0.0 {
Err("Division by zero".to_string())
} else {
Ok(numerator / denominator)
}
}
// Returning Option
fn find_in_vector(vec: &[i32], target: i32) -> Option<usize> {
vec.iter().position(|&x| x == target)
}
fn main() {
println!("Square: {}", square(5));
let (quotient, remainder) = divide(17, 5);
println!("17 / 5 = {} remainder {}", quotient, remainder);
let (x, y) = get_coordinates();
println!("Coordinates: ({}, {})", x, y);
let numbers = vec![1, 2, 3, 4, 5];
if let Some(first) = get_first_element(&numbers) {
println!("First element: {}", first);
}
match safe_divide(10.0, 2.0) {
Ok(result) => println!("Division result: {}", result),
Err(e) => println!("Error: {}", e),
}
match find_in_vector(&numbers, 3) {
Some(pos) => println!("Found at position: {}", pos),
None => println!("Not found"),
}
}
4. Statements vs Expressions
Understanding the Difference
// Statements perform actions but don't return values
// Expressions evaluate to a value
fn demonstrate_statements_expressions() {
// Statement: variable declaration
let x = 5;
// Expression: 5 + 3 evaluates to 8
let y = 5 + 3;
// Statement: println! macro call
println!("x = {}, y = {}", x, y);
// Block as expression
let z = {
let a = 10;
let b = 20;
a + b // This is an expression (no semicolon)
};
println!("z = {}", z);
// If as expression
let condition = true;
let result = if condition {
100 // Expression
} else {
200 // Expression
};
println!("result = {}", result);
// Match as expression
let number = 3;
let description = match number {
1 => "one",
2 => "two",
_ => "other",
};
println!("description = {}", description);
// Loop as expression
let mut counter = 0;
let loop_result = loop {
counter += 1;
if counter == 5 {
break counter * 2; // Expression returning value
}
};
println!("loop_result = {}", loop_result);
}
Function Bodies as Expressions
// Function body is a block expression
fn add_v1(a: i32, b: i32) -> i32 {
a + b // Expression (no semicolon) -> implicit return
}
fn add_v2(a: i32, b: i32) -> i32 {
return a + b; // Statement with explicit return
}
fn complex_computation(x: i32) -> i32 {
// The entire function body is one big expression
let y = {
let temp = x * 2;
temp + 5
};
let z = if y > 10 {
y * 3
} else {
y * 2
};
match z {
0..=20 => z + 10,
21..=40 => z * 2,
_ => z - 10,
} // This is the return value (no semicolon)
}
fn main() {
println!("add_v1: {}", add_v1(5, 3));
println!("add_v2: {}", add_v2(5, 3));
println!("complex: {}", complex_computation(7));
}
5. Function Overloading (Not Supported)
// Rust doesn't support traditional function overloading
// But you can achieve similar results with traits or generics
// Using generics instead of overloading
fn print_value<T: std::fmt::Display>(value: T) {
println!("Value: {}", value);
}
// Using different function names
fn add_i32(a: i32, b: i32) -> i32 {
a + b
}
fn add_f64(a: f64, b: f64) -> f64 {
a + b
}
// Using enum for different types
enum Input {
Integer(i32),
Float(f64),
Text(String),
}
fn process_input(input: Input) -> String {
match input {
Input::Integer(i) => format!("Integer: {}", i),
Input::Float(f) => format!("Float: {}", f),
Input::Text(s) => format!("Text: {}", s),
}
}
fn main() {
print_value(42);
print_value(3.14);
print_value("hello");
println!("i32 sum: {}", add_i32(5, 3));
println!("f64 sum: {}", add_f64(5.5, 3.3));
println!("{}", process_input(Input::Integer(42)));
println!("{}", process_input(Input::Float(3.14)));
println!("{}", process_input(Input::Text("hello".to_string())));
}
6. Generic Functions
Basic Generics
// Generic function with one type parameter
fn identity<T>(value: T) -> T {
value
}
// Generic with multiple type parameters
fn swap<A, B>(a: A, b: B) -> (B, A) {
(b, a)
}
// Generic with trait bounds
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
// Generic with where clause
fn print_display<T>(value: T)
where
T: std::fmt::Display,
{
println!("Display: {}", value);
}
fn print_debug<T>(value: T)
where
T: std::fmt::Debug,
{
println!("Debug: {:?}", value);
}
fn main() {
let x = identity(42);
let y = identity("hello");
println!("x: {}, y: {}", x, y);
let swapped = swap(5, "hello");
println!("swapped: {:?}", swapped);
let numbers = vec![34, 50, 25, 100, 65];
println!("Largest number: {}", largest(&numbers));
let chars = vec!['y', 'm', 'a', 'q'];
println!("Largest char: {}", largest(&chars));
print_display(42);
print_debug(vec![1, 2, 3]);
}
Advanced Generics
use std::fmt::Display;
use std::ops::Add;
// Generic with multiple bounds
fn compare_and_print<T: PartialOrd + Display>(a: T, b: T) {
if a > b {
println!("{} is greater than {}", a, b);
} else if a < b {
println!("{} is less than {}", a, b);
} else {
println!("{} equals {}", a, b);
}
}
// Generic with associated types
trait Container {
type Item;
fn add(&mut self, item: Self::Item);
fn get(&self, index: usize) -> Option<&Self::Item>;
}
impl<T> Container for Vec<T> {
type Item = T;
fn add(&mut self, item: T) {
self.push(item);
}
fn get(&self, index: usize) -> Option<&T> {
self.get(index)
}
}
// Generic with const generics
fn sum_array<const N: usize>(arr: [i32; N]) -> i32 {
let mut sum = 0;
for i in 0..N {
sum += arr[i];
}
sum
}
// Generic with custom trait
trait Double {
fn double(self) -> Self;
}
impl Double for i32 {
fn double(self) -> Self {
self * 2
}
}
impl Double for f64 {
fn double(self) -> Self {
self * 2.0
}
}
fn double_value<T: Double>(value: T) -> T {
value.double()
}
fn main() {
compare_and_print(10, 5);
compare_and_print(3.14, 2.71);
let mut vec = Vec::new();
vec.add(1);
vec.add(2);
println!("vec[0]: {:?}", vec.get(0));
let arr = [1, 2, 3, 4, 5];
println!("Sum of array: {}", sum_array(arr));
println!("Double i32: {}", double_value(21));
println!("Double f64: {}", double_value(3.14));
}
7. Higher-Order Functions
Functions as Parameters
// Function that takes a function as parameter
fn apply_twice<F>(f: F, x: i32) -> i32
where
F: Fn(i32) -> i32,
{
f(f(x))
}
// Function that returns a function
fn create_adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y
}
// Using function pointers
fn add_one(x: i32) -> i32 {
x + 1
}
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg)
}
// Function with different Fn traits
fn call_fn<F>(f: F)
where
F: Fn(),
{
f();
}
fn call_fn_mut<F>(mut f: F)
where
F: FnMut(),
{
f();
f();
}
fn call_fn_once<F>(f: F)
where
F: FnOnce(),
{
f();
}
fn main() {
// Passing closure
let result = apply_twice(|x| x * 2, 5);
println!("apply_twice: {}", result); // (5*2)*2 = 20
// Using returned function
let add_five = create_adder(5);
println!("add_five(3): {}", add_five(3));
// Function pointer
let result = do_twice(add_one, 5);
println!("do_twice: {}", result); // 6 + 6 = 12
// Different Fn traits
let x = 5;
let print_x = || println!("x = {}", x); // Fn
call_fn(print_x);
let mut count = 0;
let mut increment = || {
count += 1;
println!("count = {}", count);
}; // FnMut
call_fn_mut(increment);
let s = String::from("hello");
let consume = move || {
println!("s = {}", s);
}; // FnOnce
call_fn_once(consume);
}
Common Higher-Order Functions
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Map
let squares: Vec<i32> = numbers.iter().map(|&x| x * x).collect();
println!("Squares: {:?}", squares);
// Filter
let evens: Vec<i32> = numbers.iter().filter(|&&x| x % 2 == 0).copied().collect();
println!("Evens: {:?}", evens);
// Fold (reduce)
let sum: i32 = numbers.iter().fold(0, |acc, &x| acc + x);
println!("Sum: {}", sum);
// Any
let has_negative = numbers.iter().any(|&x| x < 0);
println!("Has negative: {}", has_negative);
// All
let all_positive = numbers.iter().all(|&x| x > 0);
println!("All positive: {}", all_positive);
// Find
let first_even = numbers.iter().find(|&&x| x % 2 == 0);
println!("First even: {:?}", first_even);
// Position
let position_of_5 = numbers.iter().position(|&x| x == 5);
println!("Position of 5: {:?}", position_of_5);
// Chain multiple operations
let result: i32 = numbers
.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * x)
.fold(0, |acc, x| acc + x);
println!("Sum of squares of evens: {}", result);
}
8. Closures
Basic Closures
fn main() {
// Simple closure
let add_one = |x| x + 1;
println!("add_one(5): {}", add_one(5));
// Closure with type annotations
let multiply = |x: i32, y: i32| -> i32 {
x * y
};
println!("multiply(3, 4): {}", multiply(3, 4));
// Closure capturing environment
let x = 5;
let add_x = |y| x + y;
println!("add_x(3): {}", add_x(3));
// Closure with multiple statements
let complex = |x| {
let y = x * 2;
let z = y + 5;
z * 3
};
println!("complex(5): {}", complex(5));
// Returning closure
let create_adder = |x| move |y| x + y;
let add_ten = create_adder(10);
println!("add_ten(7): {}", add_ten(7));
}
Capture Modes
fn main() {
// By reference (Fn)
let s1 = String::from("hello");
let print_ref = || println!("s1: {}", s1);
print_ref();
println!("Still have s1: {}", s1);
// By mutable reference (FnMut)
let mut s2 = String::from("hello");
let mut append_world = || {
s2.push_str(", world");
println!("s2: {}", s2);
};
append_world();
// append_world(); // Can call multiple times
println!("s2 after: {}", s2);
// By value (FnOnce)
let s3 = String::from("hello");
let consume = || {
let s = s3;
println!("s: {}", s);
};
consume();
// consume(); // Can't call twice
// println!("s3: {}", s3); // s3 moved
// Mixing capture modes
let a = String::from("a");
let mut b = String::from("b");
let c = String::from("c");
let mixed = || {
println!("a: {}", a); // by reference
b.push_str("b"); // by mutable reference
drop(c); // by value (FnOnce)
};
// mixed(); // This would be FnOnce due to drop(c)
}
Closures and Move
fn main() {
// Move semantics in closures
let v1 = vec![1, 2, 3];
let v2 = vec![4, 5, 6];
// Without move - captures by reference
let print_v1 = || {
println!("v1: {:?}", v1);
};
print_v1();
println!("Still have v1: {:?}", v1);
// With move - captures by value
let take_v2 = move || {
println!("v2: {:?}", v2);
};
take_v2();
// println!("v2: {:?}", v2); // Error: v2 moved
// Practical use: spawning threads
let data = vec![1, 2, 3, 4, 5];
std::thread::spawn(move || {
println!("Thread data: {:?}", data);
}).join().unwrap();
// Can't use data here - it's moved
// println!("Main data: {:?}", data);
}
9. Function Pointers
Using Function Pointers
// Function pointer type
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn subtract(a: i32, b: i32) -> i32 {
a - b
}
fn multiply(a: i32, b: i32) -> i32 {
a * b
}
// Function taking function pointer
fn calculate(op: fn(i32, i32) -> i32, a: i32, b: i32) -> i32 {
op(a, b)
}
// Function returning function pointer
fn get_operation(op_name: &str) -> Option<fn(i32, i32) -> i32> {
match op_name {
"add" => Some(add),
"sub" => Some(subtract),
"mul" => Some(multiply),
_ => None,
}
}
// Array of function pointers
fn main() {
let ops: [fn(i32, i32) -> i32; 3] = [add, subtract, multiply];
for (i, op) in ops.iter().enumerate() {
let result = op(10, 5);
println!("Operation {}: {}", i, result);
}
println!("add(5, 3) = {}", calculate(add, 5, 3));
println!("subtract(5, 3) = {}", calculate(subtract, 5, 3));
if let Some(op) = get_operation("add") {
println!("Dynamic operation: {}", op(15, 7));
}
// Function pointers vs closures
let closure = |a: i32, b: i32| a * b;
// Closure that doesn't capture can be coerced to fn pointer
let fn_ptr: fn(i32, i32) -> i32 = |a, b| a + b;
println!("Closure as fn pointer: {}", fn_ptr(5, 3));
// But capturing closures cannot
let x = 5;
// let capturing: fn(i32) -> i32 = |a| a + x; // Error
}
10. Diverging Functions
Functions that Never Return
// Diverging function (returns never type !)
fn forever() -> ! {
loop {
println!("This will never end!");
std::thread::sleep(std::time::Duration::from_secs(1));
}
}
fn panic_and_die() -> ! {
panic!("This function always panics");
}
// Useful in match arms
fn process_number(num: Option<i32>) -> i32 {
match num {
Some(n) => n,
None => panic!("No value provided"), // panic! returns !
}
}
// The never type in generic contexts
fn either<T>(value: Option<T>) -> T {
match value {
Some(val) => val,
None => forever(), // forever() returns !, which coerces to any type
}
}
fn main() {
// These functions never return normally
// panic_and_die();
// forever();
let result = process_number(Some(42));
println!("Result: {}", result);
// The following would panic
// let result = process_number(None);
// Using never type with loop
let x = loop {
break 5; // loop with break returns value
};
println!("x: {}", x);
// Infinite loop with break
let y = loop {
if true {
break 10;
}
};
println!("y: {}", y);
}
11. Methods
Associated Functions and Methods
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// Associated function (doesn't take self)
fn new(width: u32, height: u32) -> Rectangle {
Rectangle { width, height }
}
// Associated function that creates a square
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
// Method (takes &self)
fn area(&self) -> u32 {
self.width * self.height
}
// Method (takes &mut self)
fn set_width(&mut self, width: u32) {
self.width = width;
}
// Method that takes ownership (rare)
fn destroy(self) {
println!("Destroying rectangle: {}x{}", self.width, self.height);
}
// Getter
fn width(&self) -> u32 {
self.width
}
// Method with multiple parameters
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
fn main() {
// Call associated functions with ::
let rect = Rectangle::new(30, 50);
let square = Rectangle::square(20);
// Call methods with .
println!("Area: {}", rect.area());
println!("Can hold square: {}", rect.can_hold(&square));
let mut rect2 = Rectangle::new(10, 10);
rect2.set_width(15);
println!("New width: {}", rect2.width());
// rect2.destroy(); // Uncomment to move ownership
// println!("{:?}", rect2); // Error: rect2 moved
}
Method Chaining
struct Calculator {
value: i32,
}
impl Calculator {
fn new() -> Self {
Calculator { value: 0 }
}
fn add(mut self, x: i32) -> Self {
self.value += x;
self
}
fn subtract(mut self, x: i32) -> Self {
self.value -= x;
self
}
fn multiply(mut self, x: i32) -> Self {
self.value *= x;
self
}
fn get(self) -> i32 {
self.value
}
}
// Builder pattern
struct PersonBuilder {
name: Option<String>,
age: Option<u32>,
email: Option<String>,
}
impl PersonBuilder {
fn new() -> Self {
PersonBuilder {
name: None,
age: None,
email: None,
}
}
fn name(mut self, name: &str) -> Self {
self.name = Some(name.to_string());
self
}
fn age(mut self, age: u32) -> Self {
self.age = Some(age);
self
}
fn email(mut self, email: &str) -> Self {
self.email = Some(email.to_string());
self
}
fn build(self) -> Person {
Person {
name: self.name.expect("Name is required"),
age: self.age.unwrap_or(0),
email: self.email,
}
}
}
struct Person {
name: String,
age: u32,
email: Option<String>,
}
fn main() {
// Method chaining
let result = Calculator::new()
.add(5)
.multiply(2)
.subtract(3)
.add(10)
.get();
println!("Calculator result: {}", result);
// Builder pattern
let person = PersonBuilder::new()
.name("Alice")
.age(30)
.email("[email protected]")
.build();
println!("Person: {} ({} years old)", person.name, person.age);
}
12. Function Attributes
Inline and Other Attributes
#![allow(dead_code)]
// Inline attribute - hint to compiler to inline function
#[inline]
fn small_function(x: i32) -> i32 {
x * 2
}
// Always inline
#[inline(always)]
fn very_small(x: i32) -> i32 {
x + 1
}
// Never inline
#[inline(never)]
fn large_function(x: i32) -> i32 {
// Complex computation
let mut result = x;
for i in 0..10 {
result += i;
}
result
}
// Deprecated function
#[deprecated(since = "1.2.0", note = "Use new_function instead")]
fn old_function() {
println!("This is old");
}
// Conditional compilation
#[cfg(target_os = "linux")]
fn linux_specific() {
println!("This runs only on Linux");
}
#[cfg(not(target_os = "linux"))]
fn linux_specific() {
println!("This runs on non-Linux systems");
}
// Testing attribute
#[test]
fn test_small_function() {
assert_eq!(small_function(5), 10);
}
// Ignored test
#[test]
#[ignore]
fn expensive_test() {
// This test won't run by default
}
// Must-use attribute
#[must_use]
fn important_calculation() -> i32 {
42
}
fn main() {
println!("Small: {}", small_function(5));
println!("Very small: {}", very_small(10));
println!("Large: {}", large_function(5));
old_function();
linux_specific();
let result = important_calculation();
println!("Important: {}", result);
// Warning if we don't use result
}
13. Recursive Functions
Basic Recursion
// Simple recursive function
fn factorial(n: u64) -> u64 {
match n {
0 | 1 => 1,
_ => n * factorial(n - 1),
}
}
// Tail recursion (optimized by compiler)
fn factorial_tail(n: u64, acc: u64) -> u64 {
match n {
0 | 1 => acc,
_ => factorial_tail(n - 1, acc * n),
}
}
// Fibonacci
fn fibonacci(n: u64) -> u64 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
// More efficient Fibonacci with memoization
use std::collections::HashMap;
fn fibonacci_memo(n: u64, memo: &mut HashMap<u64, u64>) -> u64 {
if let Some(&result) = memo.get(&n) {
return result;
}
let result = match n {
0 => 0,
1 => 1,
_ => fibonacci_memo(n - 1, memo) + fibonacci_memo(n - 2, memo),
};
memo.insert(n, result);
result
}
// Tree traversal with recursion
#[derive(Debug)]
struct TreeNode<T> {
value: T,
left: Option<Box<TreeNode<T>>>,
right: Option<Box<TreeNode<T>>>,
}
impl<T: std::fmt::Debug> TreeNode<T> {
fn new(value: T) -> Self {
TreeNode {
value,
left: None,
right: None,
}
}
fn insert_left(&mut self, value: T) {
self.left = Some(Box::new(TreeNode::new(value)));
}
fn insert_right(&mut self, value: T) {
self.right = Some(Box::new(TreeNode::new(value)));
}
fn traverse_preorder(&self) {
println!("{:?}", self.value);
if let Some(left) = &self.left {
left.traverse_preorder();
}
if let Some(right) = &self.right {
right.traverse_preorder();
}
}
}
fn main() {
println!("Factorial of 5: {}", factorial(5));
println!("Tail factorial of 5: {}", factorial_tail(5, 1));
println!("Fibonacci of 10: {}", fibonacci(10));
let mut memo = HashMap::new();
println!("Memoized Fibonacci of 10: {}", fibonacci_memo(10, &mut memo));
// Tree example
let mut root = TreeNode::new(1);
root.insert_left(2);
root.insert_right(3);
if let Some(left) = &mut root.left {
left.insert_left(4);
left.insert_right(5);
}
println!("Tree traversal:");
root.traverse_preorder();
}
14. Function Safety and Best Practices
Safety Considerations
// Function with preconditions
fn divide_safe(numerator: f64, denominator: f64) -> Option<f64> {
if denominator == 0.0 {
None
} else {
Some(numerator / denominator)
}
}
// Function with invariants
struct PositiveInteger(i32);
impl PositiveInteger {
fn new(value: i32) -> Option<Self> {
if value > 0 {
Some(PositiveInteger(value))
} else {
None
}
}
fn value(&self) -> i32 {
self.0
}
}
// Unsafe function
unsafe fn dangerous_operation(ptr: *const i32) -> i32 {
*ptr
}
// Safe wrapper around unsafe code
fn safe_operation(value: &i32) -> i32 {
*value
}
// Function with explicit panics
fn get_element_at_index<T>(vec: &[T], index: usize) -> &T {
if index >= vec.len() {
panic!("Index out of bounds: {} >= {}", index, vec.len());
}
&vec[index]
}
fn main() {
// Safe division
match divide_safe(10.0, 2.0) {
Some(result) => println!("Result: {}", result),
None => println!("Division by zero"),
}
// Positive integer
if let Some(num) = PositiveInteger::new(5) {
println!("Positive integer: {}", num.value());
}
// Unsafe requires unsafe block
let x = 42;
let ptr = &x as *const i32;
unsafe {
println!("Dangerous: {}", dangerous_operation(ptr));
}
// Panic on invalid index
let vec = vec![1, 2, 3];
// println!("{}", get_element_at_index(&vec, 5)); // Panics
println!("{}", get_element_at_index(&vec, 1)); // Works
}
Function Design Best Practices
// 1. Keep functions focused (single responsibility)
fn process_user_data(data: &str) -> Result<ProcessedData, Error> {
// Only process, don't validate or save
Ok(ProcessedData::new(data))
}
// 2. Use meaningful names
// Bad
fn f(x: i32) -> i32 {
x * 2
}
// Good
fn double(value: i32) -> i32 {
value * 2
}
// 3. Limit number of parameters
// Bad
fn create_user_bad(name: &str, age: i32, email: &str, phone: &str, address: &str, active: bool) {}
// Good - use struct
struct UserConfig {
name: String,
age: i32,
email: String,
phone: Option<String>,
address: Option<String>,
active: bool,
}
fn create_user_good(config: UserConfig) {}
// 4. Return meaningful errors
fn read_file_safe(path: &str) -> Result<String, std::io::Error> {
std::fs::read_to_string(path)
}
// 5. Document functions
/// Calculates the area of a rectangle.
///
/// # Arguments
///
/// * `width` - The width of the rectangle
/// * `height` - The height of the rectangle
///
/// # Returns
///
/// The area of the rectangle as a `u32`
///
/// # Examples
///
/// ```
/// let area = rectangle_area(5, 10);
/// assert_eq!(area, 50);
/// ```
fn rectangle_area(width: u32, height: u32) -> u32 {
width * height
}
struct ProcessedData {
content: String,
}
impl ProcessedData {
fn new(content: &str) -> Self {
ProcessedData {
content: content.to_string(),
}
}
}
#[derive(Debug)]
struct Error;
fn main() {
let config = UserConfig {
name: "Alice".to_string(),
age: 30,
email: "[email protected]".to_string(),
phone: None,
address: None,
active: true,
};
create_user_good(config);
println!("Area: {}", rectangle_area(5, 10));
}
Conclusion
Rust's function system provides powerful and flexible tools for organizing code:
Key Takeaways
- Function Declaration:
fnkeyword with explicit parameter and return types - Statements vs Expressions: Understanding the difference for implicit returns
- Parameters: Pattern matching, references, and borrowing
- Return Values: Single values, tuples, Result, Option, and references
- Generics: Type-parameterized functions for code reuse
- Higher-Order Functions: Functions as parameters and return values
- Closures: Anonymous functions that capture environment
- Methods: Functions associated with types
- Function Pointers: References to functions
- Diverging Functions: Functions that never return (!)
Best Practices
- Keep functions focused on a single task
- Use meaningful names that describe purpose
- Limit the number of parameters (consider structs)
- Document public functions with doc comments
- Handle errors appropriately with Result/Option
- Use generics to avoid code duplication
- Prefer expressions over statements when appropriate
- Follow Rust naming conventions (snake_case for functions)
Function Traits Summary
| Trait | Description | Called |
|---|---|---|
Fn | Immutably captures environment | Multiple times |
FnMut | Mutably captures environment | Multiple times |
FnOnce | Consumes captured variables | Once |
Rust's function system combines the best of procedural and functional programming, providing both performance and expressiveness while maintaining strong safety guarantees through the type system and borrow checker.