Introduction to Functional Programming in Rust
Rust is a multi-paradigm language that, alongside imperative and object-oriented styles, actively supports functional programming. Built-in features such as closures, iterators, a powerful type system, and immutability by default allow you to write concise, safe, and expressive code. In this article, we will break down the key concepts of the functional approach in Rust and show how to apply them in practice.
Fundamentals of the Functional Approach: Immutability and Pure Functions
Functional programming is built on two pillars: data immutability and pure functions. Rust encourages immutability — by default, all variables are immutable. To change a value, you need to explicitly specify mut.
fn main() { let x = 5; // immutable variable // x = 6; // compilation error let mut y = 10; y += 5; // allowed, because y is mutable println!("x = {}, y = {}", x, y);}A pure function has no side effects and always returns the same result for the same arguments. Rust allows you to write such functions but does not force you to — you can freely mix styles.
fn add(a: i32, b: i32) -> i32 { a + b // pure function}Closures — Anonymous Functions
Closures in Rust are anonymous functions that can capture variables from the surrounding scope. They are defined using the syntax |parameters| body. Closures come in three types based on the capture method: by reference (&T), by mutable reference (&mut T), and by value (T).
Simple Closure Example
fn main() { let factor = 2; let multiply = |x: i32| x * factor; println!("{}", multiply(10)); // 20}Closure with Mutable Capture
fn main() { let mut counter = 0; let mut increment = || { counter += 1; counter }; println!("{}", increment()); // 1 println!("{}", increment()); // 2}Closures are widely used with iterators and for passing behavior to higher-order functions.
Iterators and Functional Chains
Iterators in Rust are one of the most powerful functional abstractions. Instead of loops, you can build chains of methods like map, filter, fold, and others. Iterators are lazy — computations only happen when a consumer (e.g., collect or for_each) is called.
Example: Filtering and Transformation
fn main() { 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 * x) // square them .collect(); println!("{:?}", result); // [4, 16, 36]}Fold — Value Accumulation
fn main() { let numbers = vec![1, 2, 3, 4]; let sum = numbers.iter().fold(0, |acc, x| acc + x); println!("Sum: {}", sum); // 10}Iterators make code declarative: you describe what needs to be done, not how. This reduces the likelihood of errors and improves readability.
Pattern Matching
The match construct is a central element of the functional style in Rust. It allows you to destructure complex data structures, handle Option and Result variants, and write exhaustive checks.
Basic Example
fn describe_number(n: i32) -> &'static str { match n { 0 => "zero", 1 | 2 => "small number", 3..=10 => "medium number", _ if n < 0 => "negative",