| Contents |

8 Functions and methods

In Rust, as in C and many other procedural or functional languages, functions are the primary tool for organizing code into named, reusable blocks. They allow you to group a sequence of statements and expressions to perform a specific task. Functions can accept input values, known as parameters, process them, and optionally produce an output value, known as a return value. This practice of breaking down programs into smaller, well-defined units is crucial for improving code readability, testability, and maintainability. Rust utilizes functions in two main ways: as standalone functions for general operations, and as methods, which are functions defined within the context of a struct, enum, or trait, typically acting upon instances of that type.

Standalone functions in Rust are also versatile: you can store them in variables, pass them as arguments to other functions, and return them as results, much like any other data type.

Rust also features anonymous functions, known as closures, which can capture variables from their surrounding environment. Closures are powerful tools and will be covered in detail in Chapter 12.

This chapter explores the core concepts of defining, calling, and utilizing both standalone functions and methods in Rust. We will cover:


8.1 The main Function: The Program’s Entry Point

Every executable Rust program must contain exactly one function named main. This function serves as the starting point when the compiled binary is executed.

fn main() {
    println!("Hello from the main function!");
}

Key characteristics of main:

8.1.1 Accessing Command-Line Arguments

You can collect command-line arguments into a Vec<String> using std::env::args():

use std::env;

fn main() {
    // The first argument (args[0]) is typically the path to the executable itself.
    let args: Vec<String> = env::args().collect();
    println!("Program path: {}", args.get(0).unwrap_or(&"Unknown".to_string()));
    println!("Arguments passed: {:?}", &args[1..]);
    // Example: Check for a specific argument
    if args.len() > 1 && args[1] == "--help" {
        println!("Displaying help information...");
        // ... logic to display help ...
    }
}

8.1.2 Returning a Result from main

Returning Result from main provides a standard way to indicate whether the program executed successfully (Ok(())) or encountered an error (Err(E)). If main returns an Err, Rust will typically print the error description to standard error and exit with a non-zero status code.

use std::fs::File;
use std::io;

// main returns a Result to indicate success or failure (specifically I/O errors).
fn main() -> Result<(), io::Error> {
    // Attempt to open a file that might not exist.
    let _f = File::open("non_existent_file.txt")?;
    // The '?' operator propagates the error if File::open fails.
    println!("File opened successfully (this won't print if the file doesn't exist).");
    // If everything succeeded, return Ok.
    Ok(())
}

This pattern simplifies error handling at the top level of your application.


8.2 Defining and Calling Functions

Rust uses the fn keyword to define functions. A key difference from C/C++ is that Rust does not require forward declarations. You can define a function anywhere in the module (usually a .rs file), and call it from code that appears earlier in the same file. The compiler processes the entire module before resolving calls.

8.2.1 Basic Function Definition Syntax

The general syntax for defining a function is:

fn function_name(parameter1: Type1, parameter2: Type2) -> ReturnType {
    // Function body: statements and expressions
    // The last expression can be the return value (if no semicolon)
}

Example:

fn main() {
    // Calling greet before its definition is allowed.
    greet("World");
    let sum = add_numbers(5, 3);
    println!("5 + 3 = {}", sum);
}

// A function that takes a string slice and prints a greeting. Returns ().
fn greet(name: &str) {
    println!("Hello, {}!", name);
}

// A function that takes two i32 integers and returns their sum.
fn add_numbers(a: i32, b: i32) -> i32 {
    a + b // This expression is the return value
}

Comparison with C: In C, if you call add_numbers before its definition, you typically need a forward declaration (prototype) like int add_numbers(int a, int b); near the top of the file or in a header file. Rust eliminates this requirement within a module.

8.2.2 Calling Functions

To call a function, use its name followed by parentheses (). If the function expects arguments, provide them inside the parentheses in the correct order and with matching types.

fn print_coordinates(x: i32, y: i32) {
    println!("Coordinates: ({}, {})", x, y);
}

// A function that takes no arguments
fn display_separator() {
    println!("--------------------");
}

fn main() {
    print_coordinates(10, 20); // Call with arguments 10 and 20.
    display_separator();       // no arguments - parentheses are still required.
}

8.2.3 Ignoring Function Return Values

If a function returns a value but you don’t need it, you can simply call the function without assigning the result to a variable.

fn get_status_code() -> u16 {
    200 // Represents an HTTP OK status
}

fn main() {
    get_status_code(); // The returned value 200 is discarded.
}

However, some functions, particularly those returning Result<T, E>, are often marked with the #[must_use] attribute. If you ignore the return value of such a function, the Rust compiler will issue a warning, as ignoring it might mean overlooking a potential error or important outcome.

#[must_use = "this Result must be handled"]
fn check_condition() -> Result<(), String> {
    // ... logic that might fail ...
    Ok(())
}

fn main() {
    check_condition(); // Compiler warning: unused result which must be used

    // To explicitly ignore a #[must_use] value:
    let _ = check_condition(); // Assigning to '_' silences the warning.
    // or simply:
    // _ = check_condition();
}

It’s generally good practice to handle or explicitly ignore Result values rather than letting them be implicitly discarded.


8.3 Function Parameters and Data Passing

Rust functions can accept parameters in various forms, each affecting ownership, mutability, and borrowing. Within a function’s body, parameters behave like ordinary variables. This section describes the fundamental parameter types, when to use them, and how they compare to C function parameters.

We will illustrate parameter passing with the String type, which is moved into the function when passed by value and can no longer be used at the call site. Note that primitive types implementing the Copy trait will be copied by value instead of moved.

8.3.1 Passing by Value (T)

When a parameter has type T (and T does not implement Copy), the value is moved into the function. The function takes ownership, and the original variable in the caller’s scope becomes inaccessible.

// This function takes ownership of the String.
fn process_string(s: String) {
    println!("Processing owned string: {}", s);
    // 's' goes out of scope here, and the memory is deallocated.
}

fn main() {
    let message = String::from("Owned data");
    process_string(message); // Ownership of 'message' is transferred to process_string.
    // Trying to use 'message' here would cause a compile-time error:
    // println!("Original message: {}", message); // Error: value borrowed after move
}

8.3.2 Passing by Mutable Value (mut T)

You can declare a value parameter as mutable using mut T. Ownership is still transferred (for non-Copy types), but the function is allowed to modify the value it now owns.

// This function takes ownership and can modify the owned value.
fn modify_string(mut s: String) { // 'mut s' allows modification inside the function
    s.push_str(" (modified)");
    println!("Modified owned string: {}", s);
    // s is dropped here unless returned
}

// Example of modifying and returning ownership
fn modify_and_return(mut s: String) -> String {
    s.push_str(" and returned");
    s // Return ownership of the modified string
}

fn main() {
    // NOTE: 'message' does NOT need to be 'mut' here!
    let message = String::from("Mutable owned data");
    // modify_string takes ownership, message cannot be used after
    modify_string(message);
    // println!("{}", message); // Error: use of moved value

    let message2 = String::from("Another message");
    // modify_and_return takes ownership, but returns it
    let modified_message2 = modify_and_return(message2);
    // println!("{}", message2); // Error: use of moved value 'message2'
    println!("{}", modified_message2); // Ok: "Another message and returned"
}

Note on Caller Variable Mutability: Notice in the examples that message and message2 were declared using let, not let mut. When passing by value (mut T), the function takes full ownership via a move. The mut in the function signature (e.g., mut s: String) only grants the function permission to mutate the value it now exclusively owns. Since the caller loses ownership and cannot access the original variable after the move, whether the original variable was declared mut is irrelevant.

This contrasts sharply with passing a mutable reference (&mut T), where the caller retains ownership and merely lends out mutable access. To grant this mutable borrow permission, the caller’s variable must be declared with let mut.

8.3.3 Passing by Shared Reference (&T)

To allow a function to read data without taking ownership, pass a shared reference (&T). This is known as borrowing. The caller retains ownership, and the data must remain valid while the reference exists.

// This function borrows the String immutably.
fn calculate_length(s: &String) -> usize {
    s.len() // Can read from 's', but cannot modify it.
}

fn main() {
    let message = String::from("Immutable borrow");
    let length = calculate_length(&message); // Pass a reference to 'message'.
    println!("The length of '{}' is {}", message, length);
    // 'message' is still valid and owned here.
}

8.3.4 Passing by Mutable Reference (&mut T)

To allow a function to modify data owned by the caller, pass a mutable reference (&mut T). This is also borrowing, but exclusively – while the mutable reference exists, no other references (mutable or shared) to the data are allowed.

// This function borrows the String mutably.
fn append_greeting(s: &mut String) {
    s.push_str(", World!"); // Can modify the borrowed String.
}

fn main() {
    // 'message' must be declared 'mut' to allow mutable borrowing.
    let mut message = String::from("Hello");
    append_greeting(&mut message); // Pass a mutable reference.
    println!("Modified message: {}", message);
    // Output: Modified message: Hello, World!
    // 'message' is still owned here, but its content has been changed.
}

8.3.5 Summary Table: Choosing Parameter Types

Parameter Type Ownership Modification of Original Caller Variable mut Required? Typical Use Case C Analogy (Approximate)
T (non-Copy) Transferred No No Consuming data, final ownership transfer Pass struct by value
T (Copy type) Copied No No Passing small, cheap-to-copy data Pass primitive by value
mut T (non-Copy) Transferred No (Local owned value) No Modifying owned value before consumption/return Pass struct by value
&T Borrowed No No Read-only access, avoiding copies const T*
&mut T Borrowed Yes Yes Modifying caller’s data in-place T* (non-const)

Note on Shadowing Parameters: You can declare a new local variable with the same name as an immutable parameter, making it mutable within the function’s scope. This is called shadowing.

fn process_value(value: i32) {
    // 'value' parameter is immutable.
    // Shadow 'value' with a new mutable variable.
    let mut value = value;
    value += 10;
    println!("Processed value: {}", value);
}

fn main() {
    process_value(5); // Prints: Processed value: 15
}

Side Note on mut with Reference Parameters: In Rust, you might occasionally encounter function signatures like fn func(mut param: &T) or fn func(mut param: &mut T). Adding mut directly before the parameter name (mut param) makes the binding param mutable within the function’s scope. This means you could reassign param to point to a different value of type &T or &mut T respectively.

This pattern of making the reference binding itself mutable is relatively uncommon in idiomatic Rust compared to simply passing &T or &mut T.


8.4 Returning Values from Functions

Functions in Rust are capable of returning values, and their return type is explicitly defined in the function signature after the -> arrow. If no return type is specified, the function implicitly returns the unit type (), which is an empty tuple.

8.4.1 Syntax for Returning Values

// Returns an i32 value.
fn give_number() -> i32 {
    42 // Implicit return of the expression's value
}

// Returns a new String.
fn create_greeting(name: &str) -> String {
    let mut greeting = String::from("Hello, ");
    greeting.push_str(name);
    greeting // Implicit return of the variable 'greeting'
}

fn main() {
    let number = give_number();
    let text = create_greeting("Alice");
    println!("Number: {}", number);
    println!("Greeting: {}", text);
}

8.4.2 Explicit return vs. Implicit Return

Rust provides two ways to specify the return value of a function:

  1. Implicit Return: If the last statement in a function body is an expression (without a trailing semicolon), its value is automatically returned. This is the idiomatic style in Rust for the common case.

    fn multiply(a: i32, b: i32) -> i32 {
        a * b // No semicolon, this expression's value is returned.
    }
  2. Explicit return Keyword: You can use the return keyword to exit the function immediately with a specific value. This is often used for early returns, such as in error conditions or conditional logic.

    fn find_first_even(numbers: &[i32]) -> Option<i32> {
        for &num in numbers {
            if num % 2 == 0 {
                return Some(num); // Early return if an even number is found.
            }
        }
        None // Implicit return if the loop finishes without finding an even number.
    }

A function’s body is fundamentally a block expression. As discussed, blocks that either are empty or conclude with a statement evaluate to the unit type ().

Therefore, if a function signature specifies a return type (e.g., -> i32), but the function’s body ends with a statement (such as an expression terminated by a semicolon, e.g., a * b;), a type mismatch error will result. This is because the function implicitly returns () instead of the expected i32. To successfully return a value, the final construct in the function body must be an expression not terminated by a semicolon.

fn multiply_buggy(a: i32, b: i32) -> i32 {
    a * b; // Semicolon makes this a statement, function returns () implicitly.
           // Compile Error: expected i32, found ()
}

Comparison with C: In C, you must use the return value; statement to return a value from a function. Functions declared void either have no return statement or use return; without a value. Rust’s implicit return from the final expression is a convenient shorthand not found in C.

8.4.3 Returning References (and Lifetimes)

Functions can return references (&T or &mut T), but this requires careful consideration of lifetimes. A returned reference must point to data that will remain valid after the function call has finished.

Typically, this means the returned reference must point to:

  1. Data that was passed into the function via a reference parameter.
  2. Data that exists outside the function (e.g., a static variable).

You cannot return a reference to a variable created locally inside the function, because that variable will be destroyed when the function exits, leaving the reference dangling (pointing to invalid memory). The Rust compiler prevents this with lifetime checks.

// This function takes a slice and returns a reference to its first element.
// The lifetime 'a ensures the returned ref. is valid as long as the input slice is.
fn get_first<'a>(slice: &'a [i32]) -> &'a i32 {
    &slice[0] // Returns a reference derived from the input slice.
}

// This function attempts to return a reference to a local variable (Compiler Error).
// fn get_dangling_reference() -> &i32 {
//     let local_value = 10;
//     &local_value // Error: `local_value` does not live long enough
// }

fn main() {
    let numbers = [10, 20, 30];
    let first = get_first(&numbers); // 'first' borrows from 'numbers'.
    println!("The first number is: {}", first);
    // 'first' remains valid as long as 'numbers' is in scope.

    // let dangling = get_dangling_reference(); // This would not compile.
}

Returning mutable references (&mut T) follows the same lifetime rules. This ability to safely return references, especially mutable ones, is a powerful feature enabled by Rust’s borrow checker, preventing common C/C++ errors like returning pointers to stack variables. Lifetimes are covered more deeply in a later chapter.


8.5 Function Scope and Nested Functions

Rust supports defining functions both at the top level of a module (similar to C) and nested within other functions.

8.5.1 Scope of Top-Level Functions

Functions defined directly within a module (not inside another function or block) are called top-level functions. They are visible throughout the entire module in which they are defined, regardless of the order of definition.

To make a top-level function accessible from other modules, you must mark it with the pub keyword (for public).

mod utils {
    // This function is private to the 'utils' module by default.
    fn helper() {
        println!("Private helper function.");
    }

    // This function is public and can be called from outside 'utils'.
    pub fn perform_task() {
        println!("Performing public task...");
        helper(); // Can call private functions within the same module.
    }
}

fn main() {
    utils::perform_task(); // OK: perform_task is public.
    // utils::helper(); // Error: helper is private.
}

8.5.2 Nested Functions

Rust allows defining functions inside the body of other functions. These are called nested functions or inner functions. A nested function is only visible and callable within the scope of the outer function where it is defined.

fn outer_function(x: i32) {
    println!("Entering outer function with x = {}", x);

    // Define a nested function.
    fn inner_function(y: i32) {
        println!("  Inner function called with y = {}", y);
        // Cannot access 'x' from outer_function here.
        // println!("  Cannot access x: {}", x); // Compile Error!
    }

    // Call the nested function.
    inner_function(x * 2);

    println!("Exiting outer function.");
}

fn main() {
    outer_function(5);
    // inner_function(10); // Error: inner_function is not in scope here.
}

Key difference from Closures: Nested functions in Rust cannot capture variables from their enclosing environment (like x in the example above). If you need a function-like construct that can access variables from its surrounding scope, you should use a closure (Chapter 12). Nested functions are simpler entities, essentially just namespaced helper functions local to another function’s implementation.


8.6 Handling Optional and Named Parameters

Unlike languages such as Python or C++, Rust does not have built-in support for:

All function arguments in Rust must be explicitly provided by the caller in the exact order specified in the function signature.

However, Rust offers idiomatic patterns to achieve similar flexibility:

8.6.1 Using Option<T> for Optional Parameters

The standard library type Option<T> (Chapter 14) can represent a value that might be present (Some(value)) or absent (None). This is commonly used to simulate optional parameters.

// 'level' is an optional parameter.
fn log_message(message: &str, level: Option<&str>) {
    // Use unwrap_or to provide a default value if 'level' is None.
    let log_level = level.unwrap_or("INFO");
    println!("[{}] {}", log_level, message);
}

fn main() {
    log_message("User logged in.", None); // Use default level "INFO".
    log_message("Disk space low!", Some("WARN")); // Provide a specific level.
}

8.6.2 The Builder Pattern for Complex Configuration

For functions with multiple configurable parameters, especially optional ones, the Builder Pattern is often used. This involves creating a separate Builder struct that accumulates configuration settings via method calls before finally constructing the desired object or performing the action.

struct WindowConfig {
    title: String,
    width: u32,
    height: u32,
    resizable: bool,
}

// Builder struct
struct WindowBuilder {
    title: String,
    width: Option<u32>,
    height: Option<u32>,
    resizable: Option<bool>,
}

impl WindowBuilder {
    // Start building with a mandatory parameter (title)
    fn new(title: String) -> Self {
        WindowBuilder {
            title,
            width: None,
            height: None,
            resizable: None,
        }
    }

    // Methods to set optional parameters
    fn width(mut self, width: u32) -> Self {
        self.width = Some(width);
        self // Return self to allow chaining
    }

    fn height(mut self, height: u32) -> Self {
        self.height = Some(height);
        self
    }

    fn resizable(mut self, resizable: bool) -> Self {
        self.resizable = Some(resizable);
        self
    }

    // Final build method using defaults for unspecified options
    fn build(self) -> WindowConfig {
        WindowConfig {
            title: self.title,
            width: self.width.unwrap_or(800),   // Default width
            height: self.height.unwrap_or(600), // Default height
            resizable: self.resizable.unwrap_or(true), // Default resizable
        }
    }
}

fn main() {
    let window1 = WindowBuilder::new("My App".to_string()).build(); // Use all defaults

    let window2 = WindowBuilder::new("Editor".to_string())
        .width(1024)
        .height(768)
        .resizable(false)
        .build(); // Specify some options

    println!("Window 1: width={}, height={}, resizable={}",
        window1.width, window1.height, window1.resizable);
    println!("Window 2: width={}, height={}, resizable={}",
        window2.width, window2.height, window2.resizable);
}

The Builder pattern provides clear, readable configuration and handles defaults gracefully, making it a robust alternative to named/default parameters for complex function calls or object construction.


8.7 Using Slices and Tuples with Functions

Slices and tuples are common data structures in Rust, frequently used as function parameters and return types. String slices were already introduced as useful function parameter types in Section 6.5.5.

8.7.1 Slices (&[T] and &str)

Slices provide a view into a contiguous sequence of elements, representing all or part of data structures like arrays, Vec<T>s, or Strings, without taking ownership. Passing slices is efficient as it only involves passing a pointer and a length.

String Slices (&str): Used for passing views of string data.

// Takes a string slice and returns the first word (also as a slice).
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]; // Return slice up to the space
        }
    }
    &s[..] // Return the whole string slice if no space is found
}

fn main() {
    let sentence = String::from("Hello beautiful world");
    let word = first_word(&sentence); // Pass reference to the String
    println!("The first word is: {}", word); // Output: The first word is: Hello
    let literal = "Another example";
    let word2 = first_word(literal); // Works directly with string literals (&str)
    println!("The first word is: {}", word2); // Output: The first word is: Another
}

Array/Vector Slices (&[T]): Used for passing views of arrays or vectors containing elements of type T.

// Calculates the sum of elements in an i32 slice.
fn sum_slice(slice: &[i32]) -> i32 {
    let mut total = 0;
    for &item in slice { // Iterate over the elements in the slice
        total += item;
    }
    total
}

fn main() {
    let numbers_array = [1, 2, 3, 4, 5];
    let numbers_vec = vec![10, 20, 30];
    println!("Sum of array: {}", sum_slice(&numbers_array[..]));
    println!("Sum of part of vec: {}", sum_slice(&numbers_vec[1..]));
}

Remember that when returning slices, lifetimes must ensure the reference remains valid (as discussed in Section 8.4.3).

As noted in Section 6.5.6, mutable slice parameters (&mut [T]) are also permitted. Functions can modify the contents of the slice, but not its length. For string slices (&mut str), an additional constraint is that all allowed modifications must preserve valid UTF-8 encoding.

8.7.2 Tuples

Tuples are fixed-size collections of values of potentially different types. They are useful for grouping related data, especially for returning multiple values from a function.

Tuples as Parameters:

// Represents a 2D point.
type Point = (i32, i32);

fn display_point(p: Point) {
    println!("Point coordinates: ({}, {})", p.0, p.1); // Access elements by index
}

fn main() {
    let my_point = (10, -5);
    display_point(my_point);
}

Tuples as Return Types: Commonly used to return multiple results without defining a dedicated struct.

// Calculates sum and product, returning them as a tuple.
fn calculate_stats(a: i32, b: i32) -> (i32, i32) {
    (a + b, a * b) // Return a tuple containing sum and product
}

fn main() {
    let num1 = 5;
    let num2 = 8;
    let (sum_result, product_result) = calculate_stats(num1, num2);
    // Destructure the returned tuple
    println!("Numbers: {}, {}", num1, num2);
    println!("Sum: {}", sum_result);
    println!("Product: {}", product_result);
}

8.8 Generic Functions

Generics allow writing functions that can operate on values of multiple different types, while still maintaining type safety. This avoids source code duplication. Generic functions declare type parameters (typically denoted by T, U, etc.) enclosed in angle brackets (<>) after the function name. These type parameters then act as placeholders for concrete types within the function’s signature (for parameters and return types) and body. Often, these type parameters require specific capabilities, expressed using trait bounds.

Generics are a large topic, covered more extensively in Chapter 11, but here’s an introduction.

Example: A Generic max function

Without generics, you’d need separate functions for i32, f64, etc.

fn max_i32(a: i32, b: i32) -> i32 {
    if a > b { a } else { b }
}
fn max_f64(a: f64, b: f64) -> f64 {
    if a > b { a } else { b }
}
// ... potentially more versions

With generics, you write one function:

use std::cmp::PartialOrd; // Trait required for comparison operators like >

// T is a type parameter.
// T: PartialOrd is a trait bound, meaning T must implement PartialOrd.
fn max_generic<T: PartialOrd>(a: T, b: T) -> T {
    if a > b {
        a
    } else {
        b
    }
}

fn main() {
    println!("Max of 5 and 10: {}", max_generic(5, 10));        // Works with i32
    println!("Max of 3.14 and 2.71: {}", max_generic(3.14, 2.71)); // Works with f64
    println!("Max of 'a' and 'z': {}", max_generic('a', 'z'));   // Works with char
}

The compiler generates specialized versions of the generic function for each concrete type used at compile time (e.g., one version for i32, one for f64). This process is called monomorphization, ensuring generic code runs just as efficiently as specialized code, without runtime overhead.


8.9 Function Pointers and Higher-Order Functions

In Rust, functions are first-class citizens. This means they can be treated like other values: assigned to variables, passed as arguments to other functions, and returned from functions.

8.9.1 Function Pointers

A variable or parameter can hold a function pointer, which references a specific function. The type of a function pointer is denoted by fn followed by the parameter types and return type. For example, fn(i32, i32) -> i32 is the type of a pointer to a function that takes two i32s and returns an i32.

type Binop = fn(i32, i32) -> i32;
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 }

// This function takes a function pointer as an argument.
fn apply_operation(operation: Binop, x: i32, y: i32) -> i32 {
    operation(x, y) // Call the function via the pointer
}

fn main() {
    let mut operation_to_perform: Binop; // Use type alias
    operation_to_perform = add; // Assign 'add' function to the pointer variable
    println!("Result of add: {}", apply_operation(operation_to_perform, 10, 5));
    operation_to_perform = subtract; // Reassign to 'subtract' function
    println!("Result of subtract: {}", apply_operation(operation_to_perform, 10, 5));
    // You can also pass the function name directly where a pointer is expected.
    println!("Directly passing multiply: {}", apply_operation(multiply, 10, 5));
}

Note: When assigning functions to variables, as in let bo: Binop = add;, the & operator is not required on the function name.

Safety and Restrictions

Function pointer types represent functions whose exact identity may not be known at compile time. Function pointers are useful for implementing callbacks, strategy patterns, or selecting behavior dynamically based on data. However, using function pointers can sometimes inhibit compiler optimizations like inlining compared to direct function calls or monomorphized generics.

8.9.2 Higher-Order Functions

A function that either takes another function as an argument or returns a function is called a higher-order function. apply_operation in the example above is a higher-order function because it takes operation (a function pointer) as an argument.

Functions can also return function pointers:

type Binop = fn(i32, i32) -> i32; // Using the type alias from before

fn get_HOF_operation(operator: char) -> Binop { // Return type is Binop
    fn add(a: i32, b: i32) -> i32 { a + b }
    fn subtract(a: i32, b: i32) -> i32 { a - b }

    match operator {
        '+' => add,      // Return a pointer to the 'add' function
        '-' => subtract, // Return a pointer to the 'subtract' function
        _ => panic!("Unknown operator"),
    }
}

fn main() {
    let op = get_HOF_operation('+');
    println!("Result (10 + 3): {}", op(10, 3)); // Call the returned function
    let op2 = get_HOF_operation('-');
    println!("Result (10 - 3): {}", op2(10, 3));
}

While function pointers are useful, closures (Chapter 12) are often more flexible in Rust because they can capture variables from their environment, whereas function pointers cannot. Higher-order functions frequently work with closures in idiomatic Rust code (e.g., methods like map, filter, fold on iterators).


8.10 Recursion and Tail Call Optimization

A function is recursive if it calls itself, either directly or indirectly. Recursion is a natural way to solve problems that can be broken down into smaller, self-similar subproblems.

8.10.1 Recursive Function Example: Factorial

The factorial function is a classic example: n! = n * (n-1)! with 0! = 1.

fn factorial(n: u64) -> u64 {
    if n == 0 {
        1 // Base case
    } else {
        n * factorial(n - 1) // Recursive step
    }
}

fn main() {
    println!("5! = {}", factorial(5)); // Output: 5! = 120
}

Each recursive call adds a new frame to the program’s call stack to store local variables, parameters, and the return address. If the recursion goes too deep (e.g., calculating factorial(100000)), it can exhaust the available stack space, leading to a stack overflow error and program crash. Recursive calls also typically incur some performance overhead compared to iterative solutions.

8.10.2 Tail Recursion and Tail Call Optimization (TCO)

A recursive call is in tail position if it is the very last action performed by the function before it returns. A function where all recursive calls are in tail position is called tail-recursive.

Example: Tail-Recursive Factorial We can rewrite factorial using an accumulator parameter to make the recursive call the last operation:

fn factorial_tailrec(n: u64, accumulator: u64) -> u64 {
    if n == 0 {
        accumulator // Base case: return the accumulated result
    } else {
        // The recursive call is the last thing done.
        factorial_tailrec(n - 1, n * accumulator)
    }
}

// Helper function to provide the initial accumulator value
fn factorial_optimized(n: u64) -> u64 {
    factorial_tailrec(n, 1) // Start with accumulator = 1
}

fn main() {
    println!("Optimized 5! = {}", factorial_optimized(5)); // Output: Optimized 5! = 120
}

Tail Call Optimization (TCO) is a compiler optimization where a tail call (especially a tail-recursive call) can be transformed into a simple jump, reusing the current stack frame instead of creating a new one. This effectively turns tail recursion into iteration, preventing stack overflow and improving performance.

Status of TCO in Rust: Critically, Rust does not currently guarantee Tail Call Optimization. While the underlying LLVM compiler backend can perform TCO in some specific situations (especially in release builds with optimizations enabled), it is not a guaranteed language feature you can rely on.

Implications: Deep recursion, even if written in a tail-recursive style, can still lead to stack overflows in Rust. For algorithms requiring deep recursion or unbounded recursion depth, you should prefer an iterative approach or simulate recursion using heap-allocated data structures (like a Vec acting as an explicit stack) if stack overflow is a concern.


8.11 Function Inlining

Inlining is a compiler optimization where the code of a called function is inserted directly at the call site, rather than performing an actual function call (which involves setting up a stack frame, jumping, and returning). Rust’s compiler (specifically, the LLVM backend) automatically performs inlining based on heuristics (function size, call frequency, optimization level, etc.) during release builds (cargo build --release).

Benefits of Inlining: Inlining primarily aims to reduce the overhead associated with function calls. More importantly, by making the function’s body visible within the caller’s context, it can unlock further optimizations:

You can influence inlining decisions using the #[inline] attribute:

// Suggest inlining this small function.
#[inline]
fn add_one(x: i32) -> i32 {
    x + 1
}

// Strongly request inlining.
#[inline(always)]
fn is_positive(x: i32) -> bool {
    x > 0
}

// Discourage inlining (rarely needed).
#[inline(never)]
fn complex_calculation(data: &[u8]) {
    // ... potentially large function body ...
    println!("Performing complex calculation.");
}


fn main() {
    let y = add_one(5);       // May be inlined
    let positive = is_positive(y); // Likely to be inlined
    complex_calculation(&[1, 2, 3]); // Unlikely to be inlined
    println!("y = {}, positive = {}", y, positive);
}

Trade-offs: While inlining reduces call overhead and enables optimizations, over-inlining (especially of large functions) can lead to code bloat, increasing the overall size of the compiled binary, which can negatively impact instruction cache performance. Relying on the compiler’s default heuristics is often sufficient, but #[inline] can be useful for performance-critical library code or very small, frequently called helper functions.

8.11.1 When Inlining Might Not Occur or Be Limited

While the compiler often performs inlining aggressively in optimized builds, certain technical and practical factors can prevent or limit it, even when hinted with #[inline] or #[inline(always)]:

Finally, enabling Link-Time Optimization (LTO) can overcome some of these boundary limitations, allowing the compiler/linker to perform more aggressive inlining across crates and codegen units, often at the cost of significantly longer link times.


8.12 Methods and Associated Functions

Rust allows associating functions directly with structs, enums, and traits using impl (implementation) blocks. These associated functions come in two main forms: methods and associated functions (often called “static methods” in other languages).

8.12.1 Defining and Calling Methods and Associated Functions

struct Circle {
    radius: f64,
}

// Implementation block for the Circle struct (Here, Self = Circle)
impl Circle {
    // Associated function: often used as a constructor.
    // Does not take 'self'. Called using Circle::new(...).
    pub fn new(radius: f64) -> Self { // 'Self' refers to the type 'Circle'
        if radius < 0.0 {
            panic!("Radius cannot be negative");
        }
        Circle { radius }
    }

    // Method: takes an immutable reference ('self: &Self').
    // Called using my_circle.area().
    pub fn area(&self) -> f64 { // Short for 'self: &Self' or 'self: &Circle'
        std::f64::consts::PI * self.radius * self.radius
    }

    // Method: takes a mutable reference ('self: &mut Self').
    // Called using my_circle.scale(...).
    pub fn scale(&mut self, factor: f64) { //Short for 'self: &mut Self' (&mut Circle)
        if factor < 0.0 {
            panic!("Scale factor cannot be negative");
        }
        self.radius *= factor;
    }

    // Method: takes ownership ('self: Self').
    // Called using my_circle.consume(). The instance cannot be used afterwards.
    pub fn consume(self) { // Short for 'self: Self' or 'self: Circle'
        println!("Consuming circle with radius {}", self.radius);
        // 'self' (the circle instance) is dropped here.
    }
}

fn main() {
    // Call associated function (constructor)
    let mut my_circle = Circle::new(5.0);
    // Call methods using dot notation
    println!("Initial Area: {}", my_circle.area());
    my_circle.scale(2.0); // Calls the mutable method
    println!("Scaled Radius: {}", my_circle.radius);
    println!("Scaled Area: {}", my_circle.area());
    // Call method that consumes the instance
    // my_circle.consume();
    // println!("Area after consume: {}", my_circle.area());
    // Error: use of moved value 'my_circle'

    // Alternative way to call methods (less common):
    // Explicitly pass the instance reference.
    let radius = 10.0;
    let another_circle = Circle::new(radius);
    let area = Circle::area(&another_circle); // Equivalent to another_circle.area()
    println!("Area of another circle: {}", area);
}

As noted in Section 6.3.1, Rust performs automatic referencing and dereferencing for method calls. When using the dot operator (object.method()), the compiler automatically inserts the appropriate &, &mut, or * to match the method’s self, &self, or &mut self receiver as required.


8.13 Function Overloading (or Lack Thereof)

Some languages allow function overloading, where multiple functions can share the same name but differ in the number or types of their parameters. The compiler selects the correct function based on the arguments provided at the call site.

Rust does not support function overloading in the traditional sense. Within a given scope, all functions must have unique names. You cannot define two functions named process where one takes an i32 and the other takes a &str.

Rust achieves similar goals using other mechanisms:

  1. Generics: As seen in Section 8.8, a single generic function can work with multiple types, provided they meet the required trait bounds.

    use std::fmt::Display;
    
    // One generic function instead of multiple overloaded versions.
    fn print_value<T: Display>(value: T) {
        println!("Value: {}", value);
    }
    
    fn main() {
        print_value(10);      // Works with i32
        print_value("hello"); // Works with &str
        print_value(3.14);    // Works with f64
    }
  2. Traits: Traits define shared behavior. Different types can implement the same trait, providing their own versions of the methods defined by that trait. This allows calling the same method name (.draw() in the example below) on different types.

    trait Draw {
        fn draw(&self);
    }
    
    struct Button { label: String }
    struct Icon { name: String }
    
    impl Draw for Button {
        fn draw(&self) { println!("Drawing button: [{}]", self.label); }
    }
    impl Draw for Icon {
        fn draw(&self) { println!("Drawing icon: <{}>", self.name); }
    }
    
    fn main() {
        let button = Button { label: "Submit".to_string() };
        let icon = Icon { name: "Save".to_string() };
        button.draw(); // Calls Button's implementation of draw
        icon.draw();   // Calls Icon's implementation of draw
    }

While not identical to overloading, generics and traits provide powerful, type-safe ways to achieve polymorphism and code reuse in Rust.


8.14 Type Inference for Function Return Types

Rust’s type inference capabilities are powerful for local variables (let x = 5; infers x is i32), but function signatures generally require explicit type annotations for both parameters and return types.

// Requires explicit parameter types and return type.
fn add(a: i32, b: i32) -> i32 {
    a + b
}

One notable exception is when using impl Trait in the return position. This syntax allows you to specify that the function returns some concrete type that implements a particular trait, without having to write out the potentially complex or unnameable concrete type itself (especially useful with closures or iterators).

// This function returns a closure. The exact type of a closure is unnameable.
// 'impl Fn(i32) -> i32' means "returns some type that implements this closure trait".
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
    // The closure captures 'x' from its environment.
    move |y| x + y
}

fn main() {
    let add_five = make_adder(5); // add_five holds the returned closure.
    println!("Result of add_five(10): {}", add_five(10)); // Output: 15

    let add_ten = make_adder(10);
    println!("Result of add_ten(7): {}", add_ten(7));   // Output: 17
}

While impl Trait provides some return type inference flexibility, you still must explicitly declare the trait(s) the returned type implements. Full return type inference like in some functional languages is generally not supported to maintain clarity and aid compile-time analysis.


8.15 Variadic Functions and Macros

C allows variadic functions – functions that can accept a variable number of arguments, like printf or scanf, using the ... syntax and stdarg.h macros.

// C Example (for comparison)
#include <stdio.h>
#include <stdarg.h>

// 'count' indicates how many numbers follow.
void print_ints(int count, ...) {
    va_list args;
    va_start(args, count); // Initialize args to retrieve arguments after 'count'.
    printf("Printing %d integers: ", count);
    for (int i = 0; i < count; i++) {
        int value = va_arg(args, int); // Retrieve next argument as an int.
        printf("%d ", value);
    }
    va_end(args); // Clean up.
    printf("\n");
}

int main() {
    print_ints(3, 10, 20, 30); // Call with 3 variable arguments.
    print_ints(5, 1, 2, 3, 4, 5); // Call with 5 variable arguments.
    return 0;
}

Variadic functions in C are powerful but lack type safety for the variable arguments, which can lead to runtime errors if the types or number of arguments retrieved using va_arg don’t match what was passed.

Rust does not support defining C-style variadic functions directly in safe code. You can call C variadic functions from Rust using FFI (Foreign Function Interface) within an unsafe block, but you cannot define your own safe variadic functions using ....

The idiomatic way to achieve similar functionality in Rust (accepting a varying number of arguments) is through macros. Macros operate at compile time, expanding code based on the arguments provided. They are type-safe and more flexible than C variadics.

// Define a macro named 'print_all'
macro_rules! print_all {
    // Match one or more expressions separated by commas
    ( $( $x:expr ),+ ) => {
        // Repeat the following code block for each matched expression '$x'
        $(
            print!("{} ", $x); // Print each expression
        )+
        println!(); // Print a newline at the end
    };
}

fn main() {
    print_all!(1, "hello", true, 3.14); // Call the macro with different types
    print_all!(100, 200);                 // Call with just integers
}

Macros like println! itself are prime examples of this pattern. They provide a type-safe, compile-time mechanism for handling variable arguments, which aligns better with Rust’s safety goals than C-style variadics. Macros are a more advanced topic covered later in the book.


8.16 Summary

This chapter provided a comprehensive look at functions and methods in Rust, contrasting them with C/C++ where relevant. Key takeaways include:

Functions and methods are central to structuring Rust code safely and efficiently. Understanding ownership, borrowing, lifetimes, and the various ways functions interact with data forms the bedrock for writing effective Rust programs. Later chapters will build on this foundation, exploring closures, asynchronous functions, and advanced trait patterns.


8.17 Exercises

Click to see the list of suggested exercises

  1. Maximum Function Variants

  2. String Concatenation Write a function concat_strings that takes two string slices (&str) as input and returns a newly allocated String containing the concatenation of the two.

    fn concat_strings(s1: &str, s2: &str) -> String {
        let mut result = String::with_capacity(s1.len()+s2.len()); // Pre-allocate cap.
        result.push_str(s1);
        result.push_str(s2);
        result // Return the new owned String
    }
    
    fn main() {
        let greeting = "Hello, ";
        let name = "Rustacean!";
        let combined = concat_strings(greeting, name);
        println!("{}", combined); // Output: Hello, Rustacean!
    }
  3. Distance Calculation Define a function distance that takes two points as tuples (f64, f64) representing (x, y) coordinates, and returns the Euclidean distance between them as an f64. Recall distance = sqrt((x2-x1)2 + (y2-y1)2).

    fn distance(p1: (f64, f64), p2: (f64, f64)) -> f64 {
        let dx = p2.0 - p1.0;
        let dy = p2.1 - p1.1;
        (dx.powi(2) + dy.powi(2)).sqrt() // Use powi(2) for squaring, then sqrt()
    }
    
    fn main() {
        let point_a = (0.0, 0.0);
        let point_b = (3.0, 4.0);
        let dist = distance(point_a, point_b);
        println!("Distance between {:?} and {:?} is {}", point_a, point_b, dist); // 5
    }
  4. Array Reversal In-Place Write a function reverse_slice that takes a mutable slice of i32 (&mut [i32]) and reverses the order of its elements in place (without creating a new array or vector).

    fn reverse_slice(slice: &mut [i32]) {
        let len = slice.len();
        if len == 0 { return; } // Handle empty slice
        let mid = len / 2;
        for i in 0..mid {
            // Swap element i with element len - 1 - i
            slice.swap(i, len - 1 - i);
        }
    }
    
    fn main() {
        let mut data1 = [1, 2, 3, 4, 5];
        reverse_slice(&mut data1);
        println!("Reversed data1: {:?}", data1); // Output: [5, 4, 3, 2, 1]
    
        let mut data2 = [10, 20, 30, 40];
        reverse_slice(&mut data2);
        println!("Reversed data2: {:?}", data2); // Output: [40, 30, 20, 10]
    
        let mut data3: [i32; 0] = []; // Empty slice
        reverse_slice(&mut data3);
        println!("Reversed empty: {:?}", data3); // Output: []
    }
  5. Find Element in Slice Write a function find_index that takes a slice of i32 (&[i32]) and a target i32 value. It should return Option<usize>, containing Some(index) if the target is found, and None otherwise. Return the index of the first occurrence.

    fn find_index(slice: &[i32], target: i32) -> Option<usize> {
        for (index, &value) in slice.iter().enumerate() {
            if value == target {
                return Some(index); // Found it, return early
            }
        }
        None // Went through the whole slice, not found
    }
    
    fn main() {
        let numbers = [10, 25, 30, 15, 25, 40];
    
        match find_index(&numbers, 30) {
            Some(idx) => println!("Found 30 at index {}", idx),
            // Output: Found 30 at index 2
            None => println!("30 not found"),
        }
    
        match find_index(&numbers, 25) {
            Some(idx) => println!("Found 25 at index {}", idx),
            // Output: Found 25 at index 1 (first occurrence)
            None => println!("25 not found"),
        }
    
        match find_index(&numbers, 99) {
            Some(idx) => println!("Found 99 at index {}", idx),
            None => println!("99 not found"), // Output: 99 not found
        }
    }

| Contents |