Skip to main content

Rust Functions and Parameters

Functions are fundamental building blocks in Rust that allow you to organize code into reusable units. Rust code uses snake case as the conventional style for function and variable names, where all letters are lowercase and underscores separate words.

Basic Function Syntax

In Rust, functions are declared using the fn keyword:

fn function_name(parameter1: type1, parameter2: type2) -> return_type {
// Function body
}

Simple Function Example

fn main() {
println!("Hello, world!");
another_function();
}

fn another_function() {
println!("Another function.");
}

When you run this program, it will output:

Hello, world!
Another function.

The execution flows in the order statements appear in the main function 1.

Functions with Parameters

Parameters are special variables that are part of a function's signature. When you define a function with parameters, you can provide concrete values (arguments) when calling the function 1.

Basic Parameter Example

fn main() {
greet("Alice");
greet("Bob");
}

fn greet(name: &str) {
println!("Hello, {}!", name);
}

Output:

Hello, Alice!
Hello, Bob!

Multiple Parameters

You can define functions with multiple parameters by separating them with commas:

fn main() {
print_info("Alice", 30);
print_info("Bob", 25);
}

fn print_info(name: &str, age: u32) {
println!("{} is {} years old", name, age);
}

Output:

Alice is 30 years old
Bob is 25 years old

Type Annotations in Function Signatures

In Rust, you must declare the type of each parameter in function signatures. This is a deliberate design decision that helps the compiler provide better error messages and reduces the need for type annotations elsewhere in your code 1.

Functions with Return Values

Functions can return values back to the caller. In Rust, you specify the return type after an arrow (->):

fn main() {
let sum = add(5, 6);
println!("The sum is: {}", sum);
}

fn add(a: i32, b: i32) -> i32 {
a + b // Note: no semicolon here - this is an expression that returns a value
}

Output:

The sum is: 11

Implicit Return with Expressions

In Rust, the last expression in a function will be returned implicitly if there's no semicolon. This is because Rust is an expression-based language 1 2.

fn calculate_area(width: u32, height: u32) -> u32 {
// The last expression is returned (no semicolon)
width * height
}

Explicit Return with the return Keyword

You can also use the return keyword to return from a function early:

fn max(a: i32, b: i32) -> i32 {
if a > b {
return a;
}
b // Implicit return if a is not greater than b
}

Use Cases for Rust Functions and Parameters

1. Code Organization and Reuse

Functions help organize code into logical, reusable units:

fn main() {
let numbers = vec![34, 50, 25, 100, 65];

println!("Numbers: {:?}", numbers);
println!("Sum: {}", sum(&numbers));
println!("Average: {:.2}", average(&numbers));
println!("Minimum: {}", min(&numbers));
println!("Maximum: {}", max(&numbers));
}

fn sum(numbers: &[i32]) -> i32 {
numbers.iter().sum()
}

fn average(numbers: &[i32]) -> f64 {
if numbers.is_empty() {
return 0.0;
}
sum(numbers) as f64 / numbers.len() as f64
}

fn min(numbers: &[i32]) -> i32 {
*numbers.iter().min().unwrap_or(&0)
}

fn max(numbers: &[i32]) -> i32 {
*numbers.iter().max().unwrap_or(&0)
}

2. Function Composition

Building complex operations by combining simpler functions:

fn main() {
let radius = 5.0;
let area = calculate_circle_area(radius);
println!("A circle with radius {} has area {:.2}", radius, area);

let side_length = 4.0;
let area = calculate_square_area(side_length);
println!("A square with side length {} has area {:.2}", side_length, area);
}

fn calculate_circle_area(radius: f64) -> f64 {
let pi = std::f64::consts::PI;
pi * square(radius)
}

fn calculate_square_area(side: f64) -> f64 {
square(side)
}

fn square(x: f64) -> f64 {
x * x
}

3. Domain-Specific Logic Encapsulation

Grouping related functionality:

fn main() {
let username = "alice_smith";
let email = "[email protected]";
let password = "password123";

if validate_username(username) && validate_email(email) && validate_password(password) {
println!("User registration data is valid");
create_user(username, email, password);
} else {
println!("Invalid user registration data");
}
}

fn validate_username(username: &str) -> bool {
let valid_length = username.len() >= 3 && username.len() <= 20;
let valid_chars = username.chars().all(|c| c.is_alphanumeric() || c == '_');

valid_length && valid_chars
}

fn validate_email(email: &str) -> bool {
// Simple email validation
email.contains('@') && email.contains('.')
}

fn validate_password(password: &str) -> bool {
password.len() >= 8
}

fn create_user(username: &str, email: &str, password: &str) {
println!("Creating user: {}", username);
// Actual user creation logic would go here
println!("User created successfully!");
}

4. Using Functions as Abstraction Barriers

Hiding implementation details behind a function interface:

fn main() {
let numbers = vec![1, 2, 3, 4, 5];

let doubled = transform_numbers(&numbers, double);
let squared = transform_numbers(&numbers, square);

println!("Original: {:?}", numbers);
println!("Doubled: {:?}", doubled);
println!("Squared: {:?}", squared);
}

fn transform_numbers(numbers: &[i32], transform_fn: fn(i32) -> i32) -> Vec<i32> {
numbers.iter().map(|&num| transform_fn(num)).collect()
}

fn double(x: i32) -> i32 {
x * 2
}

fn square(x: i32) -> i32 {
x * x
}

5. Functions with Default Values Using Option Parameters

Implementing default parameter values in Rust:

fn main() {
greet_user("Alice", None);
greet_user("Bob", Some("Admin"));
}

fn greet_user(name: &str, role: Option<&str>) {
// Unwrap the role or use "User" as default
let user_role = role.unwrap_or("User");
println!("Hello, {}! Your role is: {}", name, user_role);
}

6. Functions with Mutable Parameters

Allowing functions to modify their parameters:

fn main() {
let mut numbers = vec![1, 2, 3, 4, 5];
println!("Before: {:?}", numbers);

add_one(&mut numbers);
println!("After adding one: {:?}", numbers);

multiply_by_two(&mut numbers);
println!("After multiplying by two: {:?}", numbers);
}

fn add_one(numbers: &mut Vec<i32>) {
for num in numbers.iter_mut() {
*num += 1;
}
}

fn multiply_by_two(numbers: &mut Vec<i32>) {
for num in numbers.iter_mut() {
*num *= 2;
}
}

7. Functions with Slice Parameters for Flexibility

Using slices to accept different collection types:

fn main() {
// Working with an array
let array = [1, 2, 3, 4, 5];
println!("Sum of array: {}", sum(&array));

// Working with a vector
let vector = vec![10, 20, 30, 40, 50];
println!("Sum of vector: {}", sum(&vector));

// Working with a slice of a vector
println!("Sum of vector slice: {}", sum(&vector[1..4]));
}

// This function accepts any slice of i32 values
fn sum(numbers: &[i32]) -> i32 {
numbers.iter().sum()
}

8. Using Functions as Building Blocks for More Complex Algorithms

fn main() {
let numbers = vec![9, 2, 7, 3, 6, 4, 5, 1, 8];

println!("Original: {:?}", numbers);
println!("Even numbers: {:?}", filter_numbers(&numbers, is_even));
println!("Odd numbers: {:?}", filter_numbers(&numbers, is_odd));
println!("Prime numbers: {:?}", filter_numbers(&numbers, is_prime));
}

fn filter_numbers(numbers: &[i32], predicate: fn(i32) -> bool) -> Vec<i32> {
numbers.iter().filter(|&&n| predicate(n)).cloned().collect()
}

fn is_even(n: i32) -> bool {
n % 2 == 0
}

fn is_odd(n: i32) -> bool {
n % 2 != 0
}

fn is_prime(n: i32) -> bool {
if n <= 1 {
return false;
}
if n <= 3 {
return true;
}
if n % 2 == 0 || n % 3 == 0 {
return false;
}

let mut i = 5;
while i * i <= n {
if n % i == 0 || n % (i + 2) == 0 {
return false;
}
i += 6;
}
true
}

9. Early Returns for Error Handling

Using early returns to handle error conditions:

fn main() {
let result = divide(10, 2);
match result {
Ok(value) => println!("Result: {}", value),
Err(msg) => println!("Error: {}", msg),
}

let result = divide(10, 0);
match result {
Ok(value) => println!("Result: {}", value),
Err(msg) => println!("Error: {}", msg),
}
}

fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
return Err("Cannot divide by zero".to_string());
}

Ok(a / b)
}

10. Functions that Don't Return a Value (Unit Type)

In Rust, functions that don't explicitly return a value actually return the unit type, written as () 2:

fn main() {
let result = print_and_return_nothing();
println!("Function returned: {:?}", result);
}

// This function returns the unit type ()
fn print_and_return_nothing() -> () {
println!("I don't return anything meaningful");
// Implicitly returns () here
}

Advanced Function Concepts

Generic Functions

Functions that work with any type that satisfies certain constraints:

fn main() {
let numbers = vec![1, 2, 3, 4, 5];
println!("First: {:?}", first(&numbers));

let chars = vec!['a', 'b', 'c'];
println!("First: {:?}", first(&chars));
}

// Generic function that works with any type
fn first<T: Copy>(slice: &[T]) -> Option<T> {
if slice.is_empty() {
None
} else {
Some(slice[0])
}
}

Function Pointers

Using functions as first-class values:

fn main() {
// Array of function pointers
let operations: [fn(i32, i32) -> i32; 4] = [add, subtract, multiply, divide_safe];

let a = 10;
let b = 5;

for (i, operation) in operations.iter().enumerate() {
let operation_name = match i {
0 => "Addition",
1 => "Subtraction",
2 => "Multiplication",
3 => "Division",
_ => "Unknown",
};

println!("{}: {} and {} = {}", operation_name, a, b, operation(a, b));
}
}

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
}

fn divide_safe(a: i32, b: i32) -> i32 {
if b == 0 { 0 } else { a / b }
}

Functions are a core component in Rust programming, providing a way to organize code, promote reuse, and create clear interfaces between different parts of your program.