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 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

  • Despite the term function pointer, Rust’s function pointers are safe and type-checked. It is not possible to call invalid or uninitialized addresses, as can happen in C.
  • Their capabilities are intentionally limited: they cannot be cast to arbitrary integers or used for unchecked jumps, unlike raw pointers in unsafe C code.

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).