Rust - Function Pointers
Overview
Estimated time: 45–60 minutes
Master function pointers in Rust for storing and calling functions dynamically. Learn the difference between function pointers and closures, and implement higher-order programming patterns.
Learning Objectives
- Understand function pointer syntax and the
fn
type - Store and call functions through pointers
- Implement higher-order functions and callback patterns
- Distinguish between function pointers and closures
Prerequisites
What are Function Pointers?
Function pointers in Rust allow you to store references to functions and call them dynamically. They use the fn
type and are different from closures.
Basic Function Pointer Syntax
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn multiply(a: i32, b: i32) -> i32 {
a * b
}
fn main() {
// Function pointer type: fn(i32, i32) -> i32
let operation: fn(i32, i32) -> i32 = add;
// Call through function pointer
let result = operation(5, 3);
println!("Result: {}", result);
// Change the function pointer
let operation = multiply;
let result = operation(5, 3);
println!("Result: {}", result);
}
Expected output:
Result: 8
Result: 15
Function Pointer Types
Different Function Signatures
fn greet() {
println!("Hello!");
}
fn square(x: i32) -> i32 {
x * x
}
fn process_string(s: &str) -> String {
s.to_uppercase()
}
fn main() {
// No parameters, no return
let simple: fn() = greet;
simple();
// One parameter, returns value
let math_op: fn(i32) -> i32 = square;
println!("Square of 4: {}", math_op(4));
// String processing
let string_op: fn(&str) -> String = process_string;
println!("Processed: {}", string_op("hello world"));
// Function pointers can be stored in variables
let operations: [fn(i32) -> i32; 1] = [square];
println!("Using array: {}", operations[0](7));
}
Expected output:
Hello!
Square of 4: 16
Processed: HELLO WORLD
Using array: 49
Function Pointers as Parameters
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
}
// Higher-order function that takes a function pointer
fn calculate(x: i32, y: i32, operation: fn(i32, i32) -> i32) -> i32 {
operation(x, y)
}
// Function that returns a function pointer
fn get_operation(op_type: &str) -> fn(i32, i32) -> i32 {
match op_type {
"add" => add,
"subtract" => subtract,
"multiply" => multiply,
_ => add, // default
}
}
fn main() {
let x = 10;
let y = 5;
// Pass different functions to the same higher-order function
println!("Add: {}", calculate(x, y, add));
println!("Subtract: {}", calculate(x, y, subtract));
println!("Multiply: {}", calculate(x, y, multiply));
// Get function pointer dynamically
let op = get_operation("multiply");
println!("Dynamic operation: {}", op(x, y));
}
Expected output:
Add: 15
Subtract: 5
Multiply: 50
Dynamic operation: 50
Arrays of Function Pointers
Function Dispatch Tables
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(a: i32, b: i32) -> i32 { a / b }
fn main() {
// Array of function pointers
let operations: [fn(i32, i32) -> i32; 4] = [add, subtract, multiply, divide];
let operation_names = ["Add", "Subtract", "Multiply", "Divide"];
let x = 20;
let y = 4;
// Execute all operations
for (i, &operation) in operations.iter().enumerate() {
let result = operation(x, y);
println!("{}: {} = {}", operation_names[i], format!("{} {} {}", x,
match i { 0 => "+", 1 => "-", 2 => "*", 3 => "/", _ => "?" }, y), result);
}
// Command dispatcher pattern
println!("\nCommand dispatcher:");
let commands = [
("add", add as fn(i32, i32) -> i32),
("sub", subtract as fn(i32, i32) -> i32),
("mul", multiply as fn(i32, i32) -> i32),
("div", divide as fn(i32, i32) -> i32),
];
let command = "mul";
if let Some((_, operation)) = commands.iter().find(|(name, _)| *name == command) {
println!("Executing {}: {}", command, operation(8, 3));
}
}
Expected output:
Add: 20 + 4 = 24
Subtract: 20 - 4 = 16
Multiply: 20 * 4 = 80
Divide: 20 / 4 = 5
Command dispatcher:
Executing mul: 24
Function Pointers vs Closures
Key Differences
fn regular_function(x: i32) -> i32 {
x * 2
}
fn main() {
let multiplier = 3;
// Function pointer - no environment capture
let fn_ptr: fn(i32) -> i32 = regular_function;
// Closure - can capture environment
let closure = |x: i32| x * multiplier;
println!("Function pointer: {}", fn_ptr(5));
println!("Closure: {}", closure(5));
// Function pointers implement Copy
let fn_ptr2 = fn_ptr;
println!("Copied function pointer: {}", fn_ptr2(7));
// Function pointers can be compared
let same_fn = regular_function;
println!("Same function: {}", fn_ptr == same_fn);
}
Expected output:
Function pointer: 10
Closure: 15
Copied function pointer: 14
Same function: true
When to Use Each
// Function pointer: No environment capture, implements Copy
fn process_with_fn_ptr(data: &[i32], processor: fn(i32) -> i32) -> Vec {
data.iter().map(|&x| processor(x)).collect()
}
// Closure: Can capture environment, more flexible
fn process_with_closure(data: &[i32], processor: F) -> Vec
where
F: Fn(i32) -> i32,
{
data.iter().map(|&x| processor(x)).collect()
}
fn double(x: i32) -> i32 {
x * 2
}
fn main() {
let numbers = [1, 2, 3, 4, 5];
let factor = 10;
// Using function pointer
let doubled = process_with_fn_ptr(&numbers, double);
println!("Doubled: {:?}", doubled);
// Using closure with captured variable
let scaled = process_with_closure(&numbers, |x| x * factor);
println!("Scaled: {:?}", scaled);
// Function pointers can be used where closures are expected
let also_doubled = process_with_closure(&numbers, double);
println!("Also doubled: {:?}", also_doubled);
}
Expected output:
Doubled: [2, 4, 6, 8, 10]
Scaled: [10, 20, 30, 40, 50]
Also doubled: [2, 4, 6, 8, 10]
Advanced Function Pointer Patterns
Callback Systems
type EventCallback = fn(&str);
struct EventSystem {
callbacks: Vec,
}
impl EventSystem {
fn new() -> Self {
EventSystem {
callbacks: Vec::new(),
}
}
fn register_callback(&mut self, callback: EventCallback) {
self.callbacks.push(callback);
}
fn trigger_event(&self, event: &str) {
println!("Triggering event: {}", event);
for callback in &self.callbacks {
callback(event);
}
}
}
fn log_event(event: &str) {
println!(" [LOG] Event occurred: {}", event);
}
fn alert_event(event: &str) {
println!(" [ALERT] Important event: {}", event);
}
fn debug_event(event: &str) {
println!(" [DEBUG] Event details: {}", event);
}
fn main() {
let mut event_system = EventSystem::new();
// Register callbacks
event_system.register_callback(log_event);
event_system.register_callback(alert_event);
event_system.register_callback(debug_event);
// Trigger events
event_system.trigger_event("user_login");
println!();
event_system.trigger_event("file_uploaded");
}
Expected output:
Triggering event: user_login
[LOG] Event occurred: user_login
[ALERT] Important event: user_login
[DEBUG] Event details: user_login
Triggering event: file_uploaded
[LOG] Event occurred: file_uploaded
[ALERT] Important event: file_uploaded
[DEBUG] Event details: file_uploaded
Strategy Pattern with Function Pointers
type SortStrategy = fn(&mut [i32]);
fn bubble_sort(arr: &mut [i32]) {
let len = arr.len();
for i in 0..len {
for j in 0..len - 1 - i {
if arr[j] > arr[j + 1] {
arr.swap(j, j + 1);
}
}
}
}
fn selection_sort(arr: &mut [i32]) {
let len = arr.len();
for i in 0..len {
let mut min_idx = i;
for j in i + 1..len {
if arr[j] < arr[min_idx] {
min_idx = j;
}
}
arr.swap(i, min_idx);
}
}
fn quick_sort(arr: &mut [i32]) {
if arr.len() <= 1 {
return;
}
let pivot_index = partition(arr);
let (left, right) = arr.split_at_mut(pivot_index);
quick_sort(left);
quick_sort(&mut right[1..]);
}
fn partition(arr: &mut [i32]) -> usize {
let len = arr.len();
let pivot_index = len - 1;
let mut i = 0;
for j in 0..len - 1 {
if arr[j] <= arr[pivot_index] {
arr.swap(i, j);
i += 1;
}
}
arr.swap(i, pivot_index);
i
}
struct Sorter {
strategy: SortStrategy,
}
impl Sorter {
fn new(strategy: SortStrategy) -> Self {
Sorter { strategy }
}
fn set_strategy(&mut self, strategy: SortStrategy) {
self.strategy = strategy;
}
fn sort(&self, data: &mut [i32]) {
(self.strategy)(data);
}
}
fn main() {
let mut data1 = [64, 34, 25, 12, 22, 11, 90];
let mut data2 = data1.clone();
let mut data3 = data1.clone();
println!("Original: {:?}", data1);
// Use different sorting strategies
let mut sorter = Sorter::new(bubble_sort);
sorter.sort(&mut data1);
println!("Bubble sort: {:?}", data1);
sorter.set_strategy(selection_sort);
sorter.sort(&mut data2);
println!("Selection sort: {:?}", data2);
sorter.set_strategy(quick_sort);
sorter.sort(&mut data3);
println!("Quick sort: {:?}", data3);
}
Function Pointers with Generics
Generic Function Pointers
fn apply_twice(value: T, func: fn(T) -> T) -> T {
func(func(value))
}
fn increment(x: i32) -> i32 {
x + 1
}
fn double(x: i32) -> i32 {
x * 2
}
fn append_exclamation(s: String) -> String {
s + "!"
}
fn main() {
// Apply function twice on integers
let result = apply_twice(5, increment);
println!("Apply increment twice to 5: {}", result);
let result = apply_twice(3, double);
println!("Apply double twice to 3: {}", result);
// Apply function twice on strings
let result = apply_twice(String::from("Hello"), append_exclamation);
println!("Apply append twice: {}", result);
}
Expected output:
Apply increment twice to 5: 7
Apply double twice to 3: 12
Apply append twice: Hello!!
Real-World Examples
Plugin System
use std::collections::HashMap;
type PluginFunction = fn(&str) -> String;
struct PluginManager {
plugins: HashMap,
}
impl PluginManager {
fn new() -> Self {
PluginManager {
plugins: HashMap::new(),
}
}
fn register_plugin(&mut self, name: String, plugin: PluginFunction) {
self.plugins.insert(name, plugin);
}
fn execute_plugin(&self, name: &str, input: &str) -> Option {
self.plugins.get(name).map(|plugin| plugin(input))
}
fn list_plugins(&self) -> Vec<&String> {
self.plugins.keys().collect()
}
}
// Plugin implementations
fn uppercase_plugin(input: &str) -> String {
input.to_uppercase()
}
fn reverse_plugin(input: &str) -> String {
input.chars().rev().collect()
}
fn word_count_plugin(input: &str) -> String {
let count = input.split_whitespace().count();
format!("Word count: {}", count)
}
fn main() {
let mut plugin_manager = PluginManager::new();
// Register plugins
plugin_manager.register_plugin("uppercase".to_string(), uppercase_plugin);
plugin_manager.register_plugin("reverse".to_string(), reverse_plugin);
plugin_manager.register_plugin("wordcount".to_string(), word_count_plugin);
let input = "Hello Rust World";
println!("Input: {}", input);
println!("Available plugins: {:?}", plugin_manager.list_plugins());
// Execute plugins
if let Some(result) = plugin_manager.execute_plugin("uppercase", input) {
println!("Uppercase: {}", result);
}
if let Some(result) = plugin_manager.execute_plugin("reverse", input) {
println!("Reverse: {}", result);
}
if let Some(result) = plugin_manager.execute_plugin("wordcount", input) {
println!("Word count: {}", result);
}
}
Expected output:
Input: Hello Rust World
Available plugins: ["reverse", "wordcount", "uppercase"]
Uppercase: HELLO RUST WORLD
Reverse: dlroW tsuR olleH
Word count: Word count: 3
State Machine
#[derive(Debug, Clone, Copy, PartialEq)]
enum State {
Idle,
Running,
Paused,
Stopped,
}
type StateHandler = fn(&mut StateMachine, &str);
struct StateMachine {
current_state: State,
handlers: HashMap,
}
impl StateMachine {
fn new() -> Self {
let mut sm = StateMachine {
current_state: State::Idle,
handlers: HashMap::new(),
};
// Register state handlers
sm.handlers.insert(State::Idle, idle_handler);
sm.handlers.insert(State::Running, running_handler);
sm.handlers.insert(State::Paused, paused_handler);
sm.handlers.insert(State::Stopped, stopped_handler);
sm
}
fn handle_event(&mut self, event: &str) {
println!("Current state: {:?}, Event: {}", self.current_state, event);
if let Some(&handler) = self.handlers.get(&self.current_state) {
handler(self, event);
}
println!("New state: {:?}\n", self.current_state);
}
fn transition_to(&mut self, new_state: State) {
self.current_state = new_state;
}
}
fn idle_handler(sm: &mut StateMachine, event: &str) {
match event {
"start" => sm.transition_to(State::Running),
_ => println!(" Cannot handle '{}' in Idle state", event),
}
}
fn running_handler(sm: &mut StateMachine, event: &str) {
match event {
"pause" => sm.transition_to(State::Paused),
"stop" => sm.transition_to(State::Stopped),
_ => println!(" Cannot handle '{}' in Running state", event),
}
}
fn paused_handler(sm: &mut StateMachine, event: &str) {
match event {
"resume" => sm.transition_to(State::Running),
"stop" => sm.transition_to(State::Stopped),
_ => println!(" Cannot handle '{}' in Paused state", event),
}
}
fn stopped_handler(sm: &mut StateMachine, event: &str) {
match event {
"reset" => sm.transition_to(State::Idle),
_ => println!(" Cannot handle '{}' in Stopped state", event),
}
}
use std::collections::HashMap;
fn main() {
let mut state_machine = StateMachine::new();
// Simulate events
state_machine.handle_event("start");
state_machine.handle_event("pause");
state_machine.handle_event("resume");
state_machine.handle_event("stop");
state_machine.handle_event("reset");
state_machine.handle_event("invalid");
}
Expected output:
Current state: Idle, Event: start
New state: Running
Current state: Running, Event: pause
New state: Paused
Current state: Paused, Event: resume
New state: Running
Current state: Running, Event: stop
New state: Stopped
Current state: Stopped, Event: reset
New state: Idle
Current state: Idle, Event: invalid
Cannot handle 'invalid' in Idle state
New state: Idle
Common Pitfalls
Function Pointer vs Closure Confusion
fn main() {
let x = 10;
// This won't work - closures that capture can't be converted to fn
// let closure = |y| x + y; // Captures x
// let fn_ptr: fn(i32) -> i32 = closure; // Error!
// This works - no capture
let closure = |y: i32| y * 2; // No capture
let fn_ptr: fn(i32) -> i32 = closure; // OK
println!("Function pointer result: {}", fn_ptr(5));
// Function pointers can be used as closures
fn multiply_by_3(x: i32) -> i32 {
x * 3
}
let numbers = vec![1, 2, 3, 4];
let result: Vec = numbers.iter().map(|&x| multiply_by_3(x)).collect();
println!("Mapped: {:?}", result);
}
Best Practices
When to Use Function Pointers
- When you need to store functions without environment capture
- For callback systems and event handlers
- In plugin architectures and dispatch tables
- When implementing strategy patterns
- For performance-critical code where closure overhead matters
Function Pointer Guidelines
- Use descriptive type aliases for complex function pointer types
- Consider using closures for more flexibility when environment capture is needed
- Function pointers implement Copy, so they're cheap to pass around
- Use arrays or maps of function pointers for dispatch tables
Checks for Understanding
Question 1: Function Pointer Type
Q: What's the type of a function pointer that takes two &str parameters and returns a String?
Click to see answer
A: fn(&str, &str) -> String
Function pointer types include the parameter types and return type in their signature.
Question 2: Comparison
Q: Can you compare two function pointers for equality?
Click to see answer
A: Yes, function pointers can be compared for equality. Two function pointers are equal if they point to the same function.
Question 3: Environment Capture
Q: Can function pointers capture variables from their environment?
Click to see answer
A: No, function pointers cannot capture environment variables. Only closures can capture their environment. Function pointers are just addresses of functions.