Functional Programming in Rust: From Iterators to Monads
Rust is a multi-paradigm language that seamlessly combines imperative, object-oriented, and functional styles. Despite the fact that Rust is not a purely functional language (like Haskell), its type system and ownership model allow writing elegant, safe, and expressive code in a functional style. In this article, we will break down the key concepts of functional programming (FP) in Rust: closures, iterators, pattern matching, and working with Option and Result. This material will be useful for both beginners and experienced developers looking to deepen their understanding of Rust's functional capabilities.
Closures and Lambda Expressions
Closures in Rust are anonymous functions that can capture variables from their surrounding scope. They are the foundation of the functional approach, allowing behavior to be passed as data.
The syntax for closures is concise: |parameters| body. Closures can be passed to functions, stored in variables, and returned from functions. It's important to remember the three traits that control capture: Fn (borrowing), FnMut (mutable borrowing), and FnOnce (consumption).
fn main() { // Simple closure let add_one = |x: i32| x + 1; println!("{}", add_one(5)); // 6
// Capturing a variable let factor = 10; let multiply = |x| x * factor; println!("{}", multiply(3)); // 30
// Closure with FnOnce trait let name = String::from("Rust"); let greet = move || { println!("Hello, {}!", name); // name is moved }; greet(); // println!("{}", name); // Error! name is no longer available}The move keyword forces capture by value (move), which is often necessary when working with multithreading or passing a closure to another context.
Iterators: Lazy Processing and Method Chains
One of the strongest aspects of the functional style in Rust is the powerful iterator adapter. Instead of explicit for loops and variable mutation, you use method chains: map, filter, fold, collect, and others. Iterators in Rust are lazy — they don't perform computations until a consuming method is called (e.g., collect() or for_each()).
fn main() { let numbers = vec![1, 2, 3, 4, 5, 6];
// Functional approach: filtering and transformation let result: Vec<i32> = numbers .iter() // create an iterator .filter(|&x| x % 2 == 0) // keep only even numbers .map(|x| x * x) // square them .collect(); // collect into a vector
println!("{:?}", result); // [4, 16, 36]
// Sum using fold (reduction) let sum = numbers.iter().fold(0, |acc, x| acc + x); println!("Sum: {}", sum); // 21
// Condition checking with any and all let has_even = numbers.iter().any(|x| x % 2 == 0); println!("Has even numbers: {}", has_even); // true}Using iterators makes code declarative, readable, and less prone to errors related to indexing and array bounds. The Rust compiler often optimizes iterator chains into efficient machine code comparable to manual loops.
Pattern Matching and Destructuring
Pattern matching (match) is the heart of functional flow control in Rust. It allows safely handling all possible variants of data, especially in combination with algebraic data types (enum). Patterns can be nested, include guards, and destructure structs, tuples, and enums.
enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32),}
fn handle_