Rust Closures & Iterators â Functional Programming
In this tutorial, you'll learn about Rust Closures & Iterators. We cover key concepts, practical examples, and best practices.
Rust closures and iterators enable expressive functional programming patterns that compile to efficient, zero-cost machine code, letting you write declarative data processing pipelines without runtime overhead.
What You'll Learn
In this tutorial, you'll learn how Rust closures capture their environment, how the <a href="/design-patterns/iterator/">Iterator</a> trait powers lazy data processing, how to chain iterator adapters, and how closures enable higher-order functions for systems programming.
Why It Matters
Systems code often processes streams of data: network packets, file bytes, sensor readings, log lines. Imperative loops with index variables are error-prone and verbose. Rust's iterator combinators express data transformations concisely, with the compiler optimizing them into the same efficient assembly as hand-written loops.
Real-World Use
The Linux bpftrace tool uses Rust iterators to process trace events. Database query engines use iterator chains for filter-map-reduce pipelines. Durga Antivirus Pro's log analysis engine processes millions of log lines per second using iterator adapters with zero allocation overhead.
flowchart LR
S[Source: Vec, File, Range] --> I[Iterator]
I --> A1[.filter()]
A1 --> A2[.map()]
A2 --> A3[.take()]
A3 --> C[Consumer: collect, sum, for_each]
note: "Lazy until consumed"
Prerequisites: Traits & Generics and Error Handling. Understanding Rust generics is required.
Closure Basics
Closures are anonymous functions that can capture variables from their environment.
fn main() {
let factor = 3;
// Closure that captures factor by reference
let multiply = |x: i32| x * factor;
println!("3 * 7 = {}", multiply(7));
println!("3 * 10 = {}", multiply(10));
// Closure with type inference
let add_one = |x| x + 1;
let nums = vec![1, 2, 3];
let plus_one: Vec<i32> = nums.into_iter().map(add_one).collect();
println!("{:?}", plus_one);
}
Expected output:
3 * 7 = 21
3 * 10 = 30
[2, 3, 4]
Capturing Modes
Closures can capture by reference, by mutable reference, or by value depending on how they use captured variables.
fn main() {
// Capture by reference (Fn)
let data = vec![1, 2, 3];
let print = || println!("{:?}", data);
print(); // borrows data immutably
println!("Still accessible: {:?}", data);
// Capture by mutable reference (FnMut)
let mut counter = 0;
let mut increment = || {
counter += 1;
counter
};
println!("Counter: {}", increment());
println!("Counter: {}", increment());
// Capture by value (FnOnce)
let name = String::from("Rust");
let consume = || {
let n = name; // moves name into closure
println!("{}", n);
};
consume();
// println!("{}", name); // ERROR: name moved into closure
}
Expected output:
[1, 2, 3]
Still accessible: [1, 2, 3]
Counter: 1
Counter: 2
Rust
Iterator Basics
The <a href="/design-patterns/iterator/">Iterator</a> trait powers all iteration in Rust. Collections implement IntoIterator.
fn main() {
// Range iterator
let sum: i32 = (1..=10).sum();
println!("Sum 1 to 10: {}", sum);
// Vector iterator
let words = vec!["hello", "world", "rust", "closure"];
for word in words.iter() {
println!("Word: {}", word);
}
// Consuming iterator
let nums = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = nums.into_iter().map(|x| x * 2).collect();
println!("Doubled: {:?}", doubled);
}
Expected output:
Sum 1 to 10: 55
Word: hello
Word: world
Word: rust
Word: closure
Doubled: [2, 4, 6, 8, 10]
Iterator Adapters
Adapters transform iterators without consuming them. They are lazy and only execute when consumed.
fn main() {
let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Chain of adapters
let result: Vec<i32> = data.iter()
.filter(|&&x| x % 2 == 0) // keep evens
.map(|&x| x * x) // square them
.take(3) // take first 3
.collect(); // consume into Vec
println!("First 3 even squares: {:?}", result);
// fold/reduce
let product = data.iter().fold(1, |acc, x| acc * x);
println!("Product 1-10: {}", product);
// any/all
let all_positive = data.iter().all(|&x| x > 0);
let any_gt_100 = data.iter().any(|&x| x > 100);
println!("All positive: {}, Any > 100: {}", all_positive, any_gt_100);
}
Expected output:
First 3 even squares: [4, 16, 36]
Product 1-10: 3628800
All positive: true, Any > 100: false
Custom Iterators
You can implement the <a href="/design-patterns/iterator/">Iterator</a> trait for your own types.
struct Fibonacci {
current: u64,
next: u64,
max: Option<u64>,
}
impl Fibonacci {
fn new(max: Option<u64>) -> Self {
Fibonacci { current: 0, next: 1, max }
}
}
impl Iterator for Fibonacci {
type Item = u64;
fn next(&mut self) -> Option<Self::Item> {
let current = self.current;
if let Some(max) = self.max {
if current > max {
return None;
}
}
self.current = self.next;
self.next = current + self.next;
Some(current)
}
}
fn main() {
let fib: Vec<u64> = Fibonacci::new(Some(50)).collect();
println!("Fibonacci up to 50: {:?}", fib);
let sum: u64 = Fibonacci::new(Some(100)).filter(|x| x % 2 == 0).sum();
println!("Sum of even fibs <= 100: {}", sum);
}
Expected output:
Fibonacci up to 50: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Sum of even fibs <= 100: 44
Closures and Security
Closures enable security patterns like sandboxing. A closure can capture a limited environment and be passed to untrusted code without exposing the full context. This is used in plugin systems where plugins should only access specific resources.
Durga Antivirus Pro uses closures to define scan callbacks that capture only the relevant file metadata without exposing the full file system API to the scan engine.
Common Mistakes
1. Unnecessary collect() Calls
Chaining collect() between adapter calls forces intermediate allocations. Only collect() at the end of the chain.
2. Capturing Large Data by Value
Moving large structs into closures copies them. Capture by reference when possible, or use Rc/Arc for shared ownership.
3. Confusing iter() and into_iter()
iter() borrows elements (returns references). into_iter() takes ownership. Use iter() when you need to reuse the collection.
4. Not Using Lazy Evaluation
Iterators are lazy. If you do not call a consuming method (collect, sum, for_each), nothing happens. Ensure your chain has a consumer.
5. Modifying Captured Variables Incorrectly
Closures that modify captured variables must be FnMut. You cannot store FnMut closures in types that require Fn.
Practice Questions
1. What are the three closure traits in Rust?
Fn (immutable capture), FnMut (mutable capture), FnOnce (consuming capture). The compiler infers the most restrictive trait needed.
2. What does lazy evaluation mean for iterators?
Iterator adapters (map, filter, etc.) do not execute until a consuming method is called. This avoids intermediate allocations and allows short-circuiting with take().
3. What is the difference between iter() and into_iter()?
iter() borrows each element, returning references. into_iter() consumes the collection and returns owned elements. Use iter() when you need the collection afterward.
4. How do you implement the Iterator trait?
Implement <a href="/design-patterns/iterator/">Iterator</a> for your type with type Item and fn next(&mut self) -> Option<Self::Item>. Return None when iteration is complete.
5. Challenge: Create a LogParser iterator that reads lines from a log file, filters by log level, and yields parsed LogEntry structs.
Mini Project: Data Processing Pipeline
struct SensorReading {
timestamp: u64,
value: f64,
unit: String,
}
fn process_readings(readings: Vec<SensorReading>) {
let avg = readings.iter()
.filter(|r| r.unit == "temperature")
.map(|r| r.value)
.fold(0.0, |sum, v| sum + v)
/ readings.iter().filter(|r| r.unit == "temperature").count() as f64;
let max = readings.iter()
.filter(|r| r.unit == "pressure")
.map(|r| r.value)
.fold(f64::NEG_INFINITY, |max, v| max.max(v));
let anomalies: Vec<&SensorReading> = readings.iter()
.filter(|r| r.value > 100.0)
.collect();
println!("Avg temperature: {:.1}", avg);
println!("Max pressure: {:.1}", max);
println!("Anomalies (>100): {}", anomalies.len());
}
fn main() {
let data = vec![
SensorReading { timestamp: 1, value: 23.5, unit: "temperature".into() },
SensorReading { timestamp: 2, value: 25.0, unit: "temperature".into() },
SensorReading { timestamp: 3, value: 1012.0, unit: "pressure".into() },
SensorReading { timestamp: 4, value: 150.0, unit: "humidity".into() },
];
process_readings(data);
}
FAQ
Related Concepts
What's Next
Dive into Smart Pointers for Box, Rc, Arc, and RefCell, then explore Concurrency for threading.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro