Function as an Argument in Rust: From Beginner to Pro
\n\nHave you ever written in Python or JavaScript and easily passed a function into another function? And then switched to Rust and stumbled on a seemingly simple task? Sounds familiar. The question "How do you pass a Rust function as a parameter?" is one of the most popular on Stack Overflow. And for good reason: Rust's type system and ownership add several layers of complexity, but also open up new possibilities.
\n\nIn this article, we'll break down three main approaches: function pointers (fn), closures, and traits. You'll learn when to use which, and stop being afraid of functional style in Rust.
Why Is This Important?
\nThe ability to pass functions as parameters is not just an academic trick. It is the foundation for:
\n- \n
- Creating flexible and reusable libraries (e.g., iterators, event handlers); \n
- Implementing the "Strategy" and "Observer" patterns; \n
- Writing concise and expressive code without a bunch of if-else statements. \n
In Rust, unlike dynamic languages, the compiler strictly checks types and lifetimes. Therefore, you need to clearly understand what you are passing: just a function address, a closure that captures context, or something more complex.
\n\nApproach 1: Function Pointer — fn
\nThe simplest and most straightforward way. If your function does not capture variables from the outer environment (it is "pure"), you can use the fn type. Note: fn is a type, while Fn, FnMut, and FnOnce are traits. Don't confuse them!
fn add_one(x: i32) -> i32 {\n x + 1\n}\n\n// Function that takes another function\nfn apply_twice(f: fn(i32) -> i32, value: i32) -> i32 {\n f(f(value))\n}\n\nfn main() {\n let result = apply_twice(add_one, 5);\n println!("Result: {}", result); // 7\n}\n\nWhen to use? When the function is known at compile time, does not capture variables, and you don't need the performance of closures. Function pointers are a zero-cost abstraction; they require no allocations.
\n\nLimitations of fn
\nThe main drawback: you cannot pass a closure that captures variables. The following code will not compile:
\n\nfn main() {\n let y = 10;\n // Closure captures y\n let closure = |x: i32| -> i32 { x + y };\n // Error! expected fn item, found closure\n // let result = apply_twice(closure, 5);\n}\n\nFor this, you need the Fn, FnMut, and FnOnce traits.
Approach 2: Closures and the Fn, FnMut, FnOnce Traits
\nRust is a systems language, and it needs to know what happens with memory. Closures can capture variables in three ways:
\n- \n
- By reference (&T) — the
Fntrait. Can be called any number of times without modifying the captured data. \n - By mutable reference (&mut T) — the
FnMuttrait. Can be called many times, but the data may be modified. \n - By value (T) — the
FnOncetrait. Can be called only once, after which the closure is "consumed". \n
Example with Fn
\nfn apply_twice_fn<F>(f: F, value: i32) -> i32\nwhere\n F: Fn(i32) -> i32,\n{\n f(f(value))\n}\n\nfn main() {\n let y = 10;\n let closure = |x| x + y; // captures y by reference\n let result = apply_twice_fn(closure, 5);\n println!("Fn: {}", result); // 25\n}\n\nExample with FnMut
\nfn apply_twice_mut<F>(mut f: F, value: i32) -> i32\nwhere\n F: FnMut(i32) -> i32,\n{\n f(f(value))\n}\n\nfn main() {\n let mut counter = 0;\n let mut closure = |x: i32| -> i32 {\n