Rust Iterators and Closures: Functional Programming
Master Rust iterators and closures for functional-style programming. Covers map, filter, fold, chaining, custom iterators, and Fn trait family.
What you'll learn
- ✓Write closures and understand Fn, FnMut, and FnOnce traits
- ✓Chain iterator adapters for expressive data transformations
- ✓Build custom iterators with the Iterator trait
- ✓Use iterators to replace loops with cleaner, faster code
Prerequisites
- •Rust basics — see /blog/rust-variables-and-types
- •Ownership and borrowing — see /blog/rust-ownership-basics
- •Traits — see /blog/rust-traits-basics
Rust’s iterators and closures bring functional programming into a systems language without sacrificing performance. Iterator chains compile down to the same machine code as hand-written loops — often faster because the compiler can optimize across the entire pipeline. Closures capture their environment safely, following Rust’s ownership rules.
This guide walks through closures and iterators together, since you almost always use them in combination.
Closures: Anonymous Functions That Capture
A closure is an anonymous function that can capture variables from its surrounding scope:
fn main() {
let multiplier = 3;
// Closure that captures `multiplier`
let multiply = |x: i32| x * multiplier;
println!("{}", multiply(5)); // 15
println!("{}", multiply(10)); // 30
}
Closures infer their parameter and return types from usage, so you rarely need type annotations. But you can add them for clarity:
let add = |a: i32, b: i32| -> i32 { a + b };
How Closures Capture
Rust closures capture variables in three ways, corresponding to three traits:
Fn— borrows immutably. The closure can be called multiple times and the captured data stays unchanged.FnMut— borrows mutably. The closure can modify captured data.FnOnce— takes ownership. The closure can only be called once because it consumes the captured data.
The compiler chooses the least restrictive capture automatically:
fn main() {
// Fn — borrows name immutably
let name = String::from("Alice");
let greet = || println!("Hello, {}!", name);
greet();
greet(); // Can call multiple times
println!("{}", name); // name still usable
// FnMut — borrows count mutably
let mut count = 0;
let mut increment = || {
count += 1;
count
};
println!("{}", increment()); // 1
println!("{}", increment()); // 2
// FnOnce — takes ownership of data
let data = vec![1, 2, 3];
let consume = move || {
println!("Consumed: {:?}", data);
drop(data);
};
consume();
// consume(); // ERROR — cannot call again
// println!("{:?}", data); // ERROR — data was moved
}
The move keyword forces the closure to take ownership of all captured variables. This is essential when passing closures to threads or returning them from functions.
Closures as Function Parameters
Use trait bounds to accept closures:
fn apply_twice<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
f(f(x))
}
fn apply_and_collect<F: FnMut(i32) -> i32>(mut f: F, items: &[i32]) -> Vec<i32> {
items.iter().map(|&x| f(x)).collect()
}
fn main() {
let result = apply_twice(|x| x + 3, 5);
println!("{}", result); // 11
let mut offset = 0;
let results = apply_and_collect(|x| { offset += 1; x + offset }, &[10, 20, 30]);
println!("{:?}", results); // [11, 22, 33]
}
Returning Closures
To return a closure from a function, use impl Fn:
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
fn make_greeting(prefix: String) -> impl Fn(&str) -> String {
move |name| format!("{}, {}!", prefix, name)
}
fn main() {
let add_five = make_adder(5);
println!("{}", add_five(10)); // 15
let hello = make_greeting("Hello".to_string());
println!("{}", hello("World")); // "Hello, World!"
}
Iterators: Lazy Data Pipelines
An iterator is any type that implements the Iterator trait, which requires one method:
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
Iterators are lazy — they produce values one at a time and do no work until consumed.
Creating Iterators
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// .iter() borrows each element
for n in numbers.iter() {
println!("{}", n); // n is &i32
}
// .iter_mut() borrows mutably
let mut numbers = vec![1, 2, 3, 4, 5];
for n in numbers.iter_mut() {
*n *= 2;
}
println!("{:?}", numbers); // [2, 4, 6, 8, 10]
// .into_iter() takes ownership
let numbers = vec![1, 2, 3];
for n in numbers.into_iter() {
println!("{}", n); // n is i32
}
// numbers is consumed — cannot use it here
}
Iterator Adapters
Adapters transform iterators into new iterators. They are lazy — nothing happens until you consume the result.
map — Transform Each Element
fn main() {
let names = vec!["alice", "bob", "charlie"];
let capitalized: Vec<String> = names
.iter()
.map(|name| {
let mut chars = name.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().to_string() + chars.as_str(),
}
})
.collect();
println!("{:?}", capitalized); // ["Alice", "Bob", "Charlie"]
}
filter — Keep Elements That Match
fn main() {
let numbers: Vec<i32> = (1..=20).collect();
let even_numbers: Vec<&i32> = numbers
.iter()
.filter(|&&n| n % 2 == 0)
.collect();
println!("{:?}", even_numbers); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
}
filter_map — Filter and Transform in One Step
fn main() {
let strings = vec!["42", "hello", "93", "world", "7"];
let numbers: Vec<i32> = strings
.iter()
.filter_map(|s| s.parse::<i32>().ok())
.collect();
println!("{:?}", numbers); // [42, 93, 7]
}
filter_map is cleaner than .map().filter().map() when you want to both check and transform.
flat_map — Flatten Nested Iterators
fn main() {
let sentences = vec!["hello world", "foo bar baz"];
let words: Vec<&str> = sentences
.iter()
.flat_map(|s| s.split_whitespace())
.collect();
println!("{:?}", words); // ["hello", "world", "foo", "bar", "baz"]
}
enumerate, zip, and chain
fn main() {
let fruits = vec!["apple", "banana", "cherry"];
// enumerate adds indices
for (i, fruit) in fruits.iter().enumerate() {
println!("{}: {}", i, fruit);
}
// zip pairs elements from two iterators
let prices = vec![1.50, 0.75, 2.00];
let menu: Vec<(&str, f64)> = fruits.iter()
.copied()
.zip(prices.iter().copied())
.collect();
println!("{:?}", menu);
// chain concatenates iterators
let more_fruits = vec!["date", "elderberry"];
let all: Vec<&&str> = fruits.iter().chain(more_fruits.iter()).collect();
println!("{:?}", all);
}
take, skip, and windows
fn main() {
let numbers: Vec<i32> = (1..=100).collect();
// First 5
let first_five: Vec<&i32> = numbers.iter().take(5).collect();
println!("{:?}", first_five); // [1, 2, 3, 4, 5]
// Skip first 95
let last_five: Vec<&i32> = numbers.iter().skip(95).collect();
println!("{:?}", last_five); // [96, 97, 98, 99, 100]
// Sliding windows
let data = vec![1, 3, 5, 7, 9];
let averages: Vec<f64> = data
.windows(3)
.map(|w| w.iter().sum::<i32>() as f64 / w.len() as f64)
.collect();
println!("{:?}", averages); // [3.0, 5.0, 7.0]
}
Consuming Iterators
Consumers drive the iterator to completion and produce a final value.
collect — Gather into a Collection
use std::collections::HashMap;
fn main() {
// Into a Vec
let squares: Vec<i32> = (1..=5).map(|n| n * n).collect();
println!("{:?}", squares); // [1, 4, 9, 16, 25]
// Into a HashMap
let entries = vec![("name", "Alice"), ("role", "admin")];
let map: HashMap<&str, &str> = entries.into_iter().collect();
println!("{:?}", map);
// Into a String
let greeting: String = vec!['H', 'e', 'l', 'l', 'o'].into_iter().collect();
println!("{}", greeting);
}
fold — Reduce to a Single Value
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let sum = numbers.iter().fold(0, |acc, &n| acc + n);
println!("Sum: {}", sum); // 15
let product = numbers.iter().fold(1, |acc, &n| acc * n);
println!("Product: {}", product); // 120
// Build a string with fold
let csv = numbers
.iter()
.fold(String::new(), |mut acc, n| {
if !acc.is_empty() {
acc.push(',');
}
acc.push_str(&n.to_string());
acc
});
println!("CSV: {}", csv); // "1,2,3,4,5"
}
find, any, all, position
fn main() {
let numbers = vec![10, 20, 35, 40, 55];
let first_odd = numbers.iter().find(|&&n| n % 2 != 0);
println!("First odd: {:?}", first_odd); // Some(35)
let has_even = numbers.iter().any(|&n| n % 2 == 0);
println!("Has even: {}", has_even); // true
let all_positive = numbers.iter().all(|&n| n > 0);
println!("All positive: {}", all_positive); // true
let pos = numbers.iter().position(|&n| n > 30);
println!("First > 30 at index: {:?}", pos); // Some(2)
}
Chaining It All Together
The real power comes from composing adapters into pipelines:
#[derive(Debug)]
struct LogEntry {
level: String,
message: String,
timestamp: u64,
}
fn analyze_logs(logs: &[LogEntry]) {
// Count errors
let error_count = logs.iter()
.filter(|log| log.level == "ERROR")
.count();
println!("Errors: {}", error_count);
// Get unique error messages sorted
let mut error_messages: Vec<&str> = logs.iter()
.filter(|log| log.level == "ERROR")
.map(|log| log.message.as_str())
.collect();
error_messages.sort();
error_messages.dedup();
println!("Unique errors: {:?}", error_messages);
// Most recent 5 warnings
let recent_warnings: Vec<&str> = logs.iter()
.filter(|log| log.level == "WARN")
.map(|log| log.message.as_str())
.rev()
.take(5)
.collect();
println!("Recent warnings: {:?}", recent_warnings);
// Group counts by level
let mut counts = std::collections::HashMap::new();
for log in logs {
*counts.entry(&log.level).or_insert(0) += 1;
}
println!("Counts: {:?}", counts);
}
Building a Custom Iterator
Implement the Iterator trait on your own types:
struct Fibonacci {
a: u64,
b: u64,
}
impl Fibonacci {
fn new() -> Self {
Fibonacci { a: 0, b: 1 }
}
}
impl Iterator for Fibonacci {
type Item = u64;
fn next(&mut self) -> Option<Self::Item> {
let value = self.a;
let next = self.a.checked_add(self.b)?; // Returns None on overflow
self.a = self.b;
self.b = next;
Some(value)
}
}
fn main() {
// First 10 Fibonacci numbers
let fibs: Vec<u64> = Fibonacci::new().take(10).collect();
println!("{:?}", fibs); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
// Sum of Fibonacci numbers below 1000
let sum: u64 = Fibonacci::new()
.take_while(|&n| n < 1000)
.sum();
println!("Sum below 1000: {}", sum);
// Even Fibonacci numbers below 1000
let even_fibs: Vec<u64> = Fibonacci::new()
.take_while(|&n| n < 1000)
.filter(|n| n % 2 == 0)
.collect();
println!("Even fibs: {:?}", even_fibs);
}
Iterator for a Custom Collection
struct Matrix {
data: Vec<Vec<f64>>,
rows: usize,
cols: usize,
}
struct MatrixIter<'a> {
matrix: &'a Matrix,
row: usize,
col: usize,
}
impl Matrix {
fn new(data: Vec<Vec<f64>>) -> Self {
let rows = data.len();
let cols = if rows > 0 { data[0].len() } else { 0 };
Matrix { data, rows, cols }
}
fn iter(&self) -> MatrixIter<'_> {
MatrixIter {
matrix: self,
row: 0,
col: 0,
}
}
}
impl<'a> Iterator for MatrixIter<'a> {
type Item = (usize, usize, f64);
fn next(&mut self) -> Option<Self::Item> {
if self.row >= self.matrix.rows {
return None;
}
let value = self.matrix.data[self.row][self.col];
let result = (self.row, self.col, value);
self.col += 1;
if self.col >= self.matrix.cols {
self.col = 0;
self.row += 1;
}
Some(result)
}
}
fn main() {
let matrix = Matrix::new(vec![
vec![1.0, 2.0, 3.0],
vec![4.0, 5.0, 6.0],
]);
for (r, c, val) in matrix.iter() {
println!("[{},{}] = {}", r, c, val);
}
let sum: f64 = matrix.iter().map(|(_, _, v)| v).sum();
println!("Sum: {}", sum); // 21.0
}
Performance: Iterators vs Loops
Iterator chains and for loops compile to the same assembly in most cases. Rust’s zero-cost abstractions mean the compiler inlines closures and eliminates iterator overhead. In benchmarks, iterator chains sometimes outperform hand-written loops because the compiler can apply SIMD optimizations more easily.
// These produce identical assembly:
fn sum_loop(data: &[i32]) -> i32 {
let mut sum = 0;
for &n in data {
sum += n;
}
sum
}
fn sum_iter(data: &[i32]) -> i32 {
data.iter().sum()
}
Use whichever style is clearer for your specific case. Iterators tend to be cleaner for transformation pipelines; loops can be clearer for complex stateful operations.
Wrapping Up
Closures and iterators are the foundation of idiomatic Rust. The core ideas are:
- Closures capture variables following ownership rules. Use
Fn,FnMut, orFnOncebounds depending on how the closure uses captured data. - Iterators are lazy — adapters like
map,filter, andflat_mapbuild a pipeline that runs only when consumed. - Consumers like
collect,fold,sum,any, andfinddrive the pipeline to produce results. - Custom iterators implement one method:
next(&mut self) -> Option<Self::Item>. - There is no runtime cost — iterator chains compile to the same efficient code as hand-written loops.
Once iterator chains become natural to you, you will find yourself reaching for them constantly. They make data transformations concise, composable, and safe.
Related articles
- Rust Rust Closures and Fn Traits Explained
Master Rust closures, understand Fn, FnMut, and FnOnce traits, learn how closures capture variables, and use closures effectively in your code.
- Rust Rust Iterators and Adapters
Master Rust's iterator trait, lazy adapters like map and filter, and consumers like collect and fold for fast, expressive data processing.
- Rust Rust Async Programming with Tokio: Getting Started
Learn async Rust with Tokio from scratch. Covers async/await syntax, spawning tasks, channels, TCP servers, and common pitfalls with working examples.
- Rust Rust Concurrency: Channels, Mutex, and Arc Patterns
Learn Rust concurrency with channels, Mutex, Arc, and thread-safe patterns. Covers mpsc, shared state, thread pools, and real-world examples.