Functional Programming in Rust: Advanced Level
Rust is a multi-paradigm language that seamlessly combines imperative and functional approaches. For developers already familiar with the basics of Rust, a world of advanced functional techniques opens up, allowing you to write more concise, safe, and expressive code. In this article, we will explore key concepts of functional programming (FP) at an advanced level: from deep use of closures and iterators to function composition and emulation of monads.
1. Advanced Closures: Ownership, Borrowing, and Traits
Closures in Rust are not just anonymous functions. The compiler automatically implements one of the traits for them: Fn, FnMut, or FnOnce. Understanding how a closure captures variables from its environment is critical for writing efficient code.
Move closure is used when the closure must own the captured data, for example, when passing it to a new thread:
use std::thread;
let data = vec![1, 2, 3];
let handle = thread::spawn(move || { println!("{:?}", data); // data moved into the closure});
handle.join().unwrap();Closures as function arguments allow creating flexible APIs. Use generic types with trait bounds:
fn apply_twice<F>(f: F, x: i32) -> i32where F: Fn(i32) -> i32,{ f(f(x))}
let square = |x| x * x;println!("{}", apply_twice(square, 2)); // Output: 16Returning closures is a complex task due to the unknown size of the closure type. Use Box<dyn Fn> or the impl keyword:
fn make_adder(x: i32) -> impl Fn(i32) -> i32 { move |y| x + y}
let add_five = make_adder(5);println!("{}", add_five(10)); // 152. Iterators: Laziness, Combinators, and Performance
Iterators in Rust are a powerful functional tool. They are lazy: computations only happen when a terminal method is called (e.g., collect(), for_each()).
Chains of combinators allow building complex data transformations without loops:
let numbers = vec![1, 2, 3, 4, 5, 6];
let result: Vec<i32> = numbers .iter() .filter(|&&x| x % 2 == 0) // keep even numbers .map(|&x| x * 2) // multiply by 2 .take(2) // take the first two .collect();
println!("{:?}", result); // [4, 8]Custom iterators are created by implementing the Iterator trait. For example, a Fibonacci number iterator:
struct Fibonacci { curr: u64, next: u64,}
impl Fibonacci { fn new() -> Self { Fibonacci { curr: 0, next: 1 } }}
impl Iterator for Fibonacci { type Item = u64;
fn next(&mut self) -> Option<Self::Item> { let new_next = self.curr.checked_add(self.next)?; self.curr = self.next; self.next = new_next; Some(self.curr) }}
let fib: Vec<u64> = Fibonacci::new().take(10).collect();println!("{:?}", fib); // [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]Iterator performance is often on par with manual loops, and in some cases the LLVM compiler generates more optimal code.
3. Function Composition and Monadic Style
Rust does not have built-in function composition, but it is easy to implement. Composition allows building new functions from existing ones.
Simple composition using closures:
fn compose<F, G, A, B, C>(f: F, g: G) -> impl Fn(A) -> Cwhere F: Fn(B) -> C, G: Fn(A) -> B,{ move |x| f(g(x))}
let add_o