Functional Programming in Rust: From A to Z
The Rust language, known for its safe memory management and high performance, also offers powerful tools for functional programming. Unlike purely functional languages (Haskell), Rust allows flexible combination of imperative and functional styles. In this article, we will break down the key concepts of functional programming in Rust: from basic closures to advanced monadic patterns. The material will be useful for both beginners and experienced developers who want to write more concise and reliable code.
1. Basics: Immutability and Variables
Functional programming begins with immutability. In Rust, variables are immutable by default. This is a key difference from many languages.
// Immutable variable (by default)let x = 10;// x = 20; // Compilation error!
// Mutable variable (explicit)let mut y = 5;y = 15; // WorksUsing let without mut guarantees that the value will not change after initialization. This reduces the number of side effects and makes the code more predictable. In a functional style, it is recommended to prefer immutable variables, creating new values instead of modifying old ones.
2. Closures — Anonymous Functions
Closures are anonymous functions that can capture variables from their environment. In Rust, they are implemented through the Fn, FnMut, and FnOnce traits.
2.1. Syntax and Variable Capture
// Simple closurelet add_one = |x: i32| -> i32 { x + 1 };println!("{}", add_one(5)); // 6
// Capturing a variable from the environmentlet base = 10;let add_base = |x| x + base; // base is captured by reference (&)println!("{}", add_base(20)); // 30
// Capture with mutation (FnMut)let mut counter = 0;let mut increment = || { counter += 1; };increment();increment();println!("{}", counter); // 2
// Capture with consumption (FnOnce)let name = String::from("Rust");let consume = || { drop(name); };consume();// println!("{}", name); // Error: name has been moved2.2. Closures as Function Arguments
Closures are often passed to higher-order functions such as map, filter, and fold.
let numbers = vec![1, 2, 3, 4, 5];
// Using a closure with filterlet evens: Vec<i32> = numbers .into_iter() .filter(|&x| x % 2 == 0) .collect();
println!("{:?}", evens); // [2, 4]3. Iterators and Transformation Chains
Iterators are the heart of functional programming in Rust. They are lazy (computations happen only when collect() or for is called) and allow building transformation chains.
3.1. Basic Iterator Adapters
let data = vec![1, 2, 3, 4, 5, 6];
// Chain: filter -> map -> take -> collectlet result: Vec<i32> = data .iter() .filter(|&x| x % 2 == 0) // keep even numbers .map(|x| x * 2) // double them .take(2) // take the first two .collect();
println!("{:?}", result); // [4, 8]3.2. Fold and Reduction
fold is the equivalent of reduce in other languages. It accumulates a result by iterating over all elements.
let numbers = [1, 2, 3, 4, 5];
// Sum of all numberslet sum = numbers.iter().fold(0, |acc, &x| acc + x);println!("Sum: {}", sum); // 15
// Factoriallet factorial = (1..=5).fold(1, |acc, x| acc * x);println!("5! = {}", factorial); // 1203.3. Lazy Infinite Iterators
// Infinite iterator, take the first 10let first_ten: Vec<i32> = (0..) .filter(|x| x % 2 == 0) .map(|x| x * x)