Series: Learn Rust from Scratch | Post 10 of 11
Introduction
Rust is a multi-paradigm language — it supports both imperative and functional programming styles. Closures and iterators are the heart of Rust's functional side.
They let you express transformations like "filter this list, transform each element, and collect the result" in a single, readable chain — all with zero performance overhead compared to a manual loop.
Closures — Anonymous Functions That Capture Their Environment
A closure is a function you can define inline, store in a variable, or pass to another function. Unlike regular functions, closures can capture variables from the scope they're defined in.
Basic Closure Syntax
fn main() {
// Regular function
fn add_one_fn(x: i32) -> i32 { x + 1 }
// Closure — same thing, more compact
let add_one = |x: i32| -> i32 { x + 1 };
// Closures can infer types — even more compact
let add_one_short = |x| x + 1;
println!("{}", add_one_fn(5)); // 6
println!("{}", add_one(5)); // 6
println!("{}", add_one_short(5)); // 6
// No-argument closure
let say_hello = || println!("Hello!");
say_hello();
// Multi-line closure
let greet = |name: &str| {
let message = format!("Welcome, {}!", name);
println!("{}", message);
};
greet("Neo");
}
Output:
6
6
6
Hello!
Welcome, Neo!
Closures Capture Their Environment
The key difference between closures and functions: closures can access variables from their surrounding scope.
fn main() {
let threshold = 10; // Outer variable
// The closure CAPTURES `threshold` from the environment
let is_above_threshold = |x: i32| x > threshold;
let numbers = vec![5, 12, 3, 18, 7, 22];
for &n in &numbers {
if is_above_threshold(n) {
println!("{} is above threshold ({})", n, threshold);
}
}
}
Output:
12 is above threshold (10)
18 is above threshold (10)
22 is above threshold (10)
How Closures Capture: Borrow, Mutable Borrow, or Move
fn main() {
// 1. Immutable borrow — closure reads the variable
let message = String::from("Hello");
let print_msg = || println!("{}", message);
print_msg();
print_msg(); // Can call multiple times — just borrows
println!("message is still: {}", message);
// 2. Mutable borrow — closure modifies the variable
let mut count = 0;
let mut increment = || {
count += 1;
println!("count = {}", count);
};
increment();
increment();
// println!("{}", count); // ❌ Can't use count while mutably borrowed
drop(increment); // Release the mutable borrow
println!("final count = {}", count); // ✅ Now we can
// 3. Move — closure takes ownership
let data = vec![1, 2, 3];
let owns_data = move || println!("Data: {:?}", data);
owns_data();
// println!("{:?}", data); // ❌ data was moved into the closure
}
Output:
Hello
Hello
message is still: Hello
count = 1
count = 2
final count = 2
Data: [1, 2, 3]
Closures as Function Parameters
Closures are used heavily as parameters to higher-order functions:
// Accept a closure as a parameter
fn apply<F: Fn(i32) -> i32>(f: F, value: i32) -> i32 {
f(value)
}
fn apply_twice<F: Fn(i32) -> i32>(f: F, value: i32) -> i32 {
f(f(value))
}
fn main() {
let double = |x| x * 2;
let add_ten = |x| x + 10;
let square = |x: i32| x * x;
println!("{}", apply(double, 5)); // 10
println!("{}", apply(add_ten, 5)); // 15
println!("{}", apply_twice(double, 3)); // 12 (3→6→12)
println!("{}", apply_twice(add_ten, 0)); // 20 (0→10→20)
println!("{}", apply(square, 4)); // 16
}
Output:
10
15
12
20
16
The Three Closure Traits
| Trait | What it means | Called |
|---|---|---|
Fn |
Borrows from environment immutably | Many times |
FnMut |
Borrows from environment mutably | Many times |
FnOnce |
Takes ownership of captured values | Only once |
fn call_once<F: FnOnce() -> String>(f: F) {
println!("Result: {}", f());
// f(); // ❌ Can't call again — FnOnce was consumed
}
fn call_many<F: Fn() -> String>(f: F) {
println!("{}", f());
println!("{}", f());
println!("{}", f());
}
fn main() {
// FnOnce: takes ownership of `name`
let name = String::from("Neo");
call_once(move || format!("Hello, {}!", name));
// Fn: only borrows, can be called repeatedly
let greeting = "Hi there";
call_many(|| greeting.to_string());
}
Output:
Result: Hello, Neo!
Hi there
Hi there
Hi there
Iterators — Lazily Processing Sequences
An iterator is anything that implements the Iterator trait, which requires one method:
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
Iterators are lazy — they don't do any work until you consume them.
Creating Iterators
fn main() {
let v = vec![1, 2, 3, 4, 5];
// .iter() — yields &T (immutable references)
// .iter_mut() — yields &mut T (mutable references)
// .into_iter() — yields T (consumes the collection)
let mut iter = v.iter();
println!("{:?}", iter.next()); // Some(1)
println!("{:?}", iter.next()); // Some(2)
println!("{:?}", iter.next()); // Some(3)
println!("{:?}", iter.next()); // Some(4)
println!("{:?}", iter.next()); // Some(5)
println!("{:?}", iter.next()); // None — exhausted
}
Output:
Some(1)
Some(2)
Some(3)
Some(4)
Some(5)
None
Iterator Adapters
Adapters transform an iterator into another iterator. They're lazy — no work happens until you consume the chain.
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// map: transform each element
let doubled: Vec<i32> = numbers.iter()
.map(|&x| x * 2)
.collect();
println!("Doubled: {:?}", doubled);
// filter: keep elements matching a condition
let evens: Vec<&i32> = numbers.iter()
.filter(|&&x| x % 2 == 0)
.collect();
println!("Evens: {:?}", evens);
// filter_map: filter AND transform in one step
let even_halved: Vec<i32> = numbers.iter()
.filter_map(|&x| if x % 2 == 0 { Some(x / 2) } else { None })
.collect();
println!("Even/2: {:?}", even_halved);
// take: only the first N elements
let first_three: Vec<&i32> = numbers.iter().take(3).collect();
println!("First 3: {:?}", first_three);
// skip: drop the first N elements
let after_five: Vec<&i32> = numbers.iter().skip(5).collect();
println!("After 5: {:?}", after_five);
// enumerate: add index
let indexed: Vec<(usize, &i32)> = numbers.iter().enumerate().take(3).collect();
println!("Indexed: {:?}", indexed);
// chain: combine two iterators
let a = vec![1, 2, 3];
let b = vec![4, 5, 6];
let combined: Vec<&i32> = a.iter().chain(b.iter()).collect();
println!("Chained: {:?}", combined);
// zip: pair elements from two iterators
let names = vec!["Alice", "Bob", "Carol"];
let scores = vec![95, 82, 91];
let paired: Vec<(&&str, &i32)> = names.iter().zip(scores.iter()).collect();
println!("Zipped: {:?}", paired);
// flat_map: map + flatten
let words = vec!["hello world", "foo bar"];
let chars: Vec<&str> = words.iter()
.flat_map(|s| s.split_whitespace())
.collect();
println!("Words: {:?}", chars);
}
Output:
Doubled: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
Evens: [2, 4, 6, 8, 10]
Even/2: [1, 2, 3, 4, 5]
First 3: [1, 2, 3]
After 5: [6, 7, 8, 9, 10]
Indexed: [(0, 1), (1, 2), (2, 3)]
Chained: [1, 2, 3, 4, 5, 6]
Zipped: [("Alice", 95), ("Bob", 82), ("Carol", 91)]
Words: ["hello", "world", "foo", "bar"]
Consuming Adapters
Consuming adapters call next() internally and return a single final value:
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// sum and product
let sum: i32 = numbers.iter().sum();
let product: i32 = numbers.iter().product();
println!("Sum: {}, Product: {}", sum, product);
// count
let count = numbers.iter().filter(|&&x| x > 5).count();
println!("Count > 5: {}", count);
// max and min (return Option)
println!("Max: {:?}", numbers.iter().max());
println!("Min: {:?}", numbers.iter().min());
// any and all
println!("Any even? {}", numbers.iter().any(|&x| x % 2 == 0));
println!("All positive? {}", numbers.iter().all(|&x| x > 0));
// find: first element matching a condition
let first_gt_5 = numbers.iter().find(|&&x| x > 5);
println!("First > 5: {:?}", first_gt_5);
// position: index of first match
let pos = numbers.iter().position(|&x| x == 7);
println!("Position of 7: {:?}", pos);
// fold: custom accumulator
let sum_of_squares: i32 = numbers.iter().fold(0, |acc, &x| acc + x * x);
println!("Sum of squares: {}", sum_of_squares);
// collect into different types
let set: std::collections::HashSet<i32> = numbers.iter().copied().collect();
println!("As HashSet: {:?}", set.len()); // 10 unique values
}
Output:
Sum: 55, Product: 3628800
Count > 5: 5
Max: Some(10)
Min: Some(1)
Any even? true
All positive? true
First > 5: Some(6)
Position of 7: Some(6)
Sum of squares: 385
As HashSet: 10
Creating a Custom Iterator
You can make your own types iterable by implementing the Iterator trait:
// A Fibonacci iterator
struct Fibonacci {
current: u64,
next: u64,
}
impl Fibonacci {
fn new() -> Self {
Fibonacci { current: 0, next: 1 }
}
}
impl Iterator for Fibonacci {
type Item = u64;
fn next(&mut self) -> Option<Self::Item> {
let result = self.current;
let new_next = self.current + self.next;
self.current = self.next;
self.next = new_next;
Some(result) // This iterator is infinite — it never returns None
}
}
fn main() {
// Take the first 10 Fibonacci numbers
let fibs: Vec<u64> = Fibonacci::new().take(10).collect();
println!("Fibonacci: {:?}", fibs);
// Because it's an iterator, ALL iterator methods work!
let sum: u64 = Fibonacci::new().take(10).sum();
println!("Sum of first 10: {}", sum);
// First Fibonacci number greater than 1000
let first_large = Fibonacci::new().find(|&n| n > 1000);
println!("First fib > 1000: {:?}", first_large);
// Count Fibonacci numbers below 100
let count = Fibonacci::new().take_while(|&n| n < 100).count();
println!("Fib numbers < 100: {}", count);
}
Output:
Fibonacci: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Sum of first 10: 88
First fib > 1000: Some(1597)
Fib numbers < 100: 11
Closures + Iterators: The Power Combo
fn main() {
let students = vec![
("Alice", 95, "Math"),
("Bob", 72, "Science"),
("Carol", 88, "Math"),
("Dave", 61, "Science"),
("Eve", 91, "Math"),
("Frank", 55, "Science"),
];
// Get names of Math students with score >= 90, sorted alphabetically
let mut top_math: Vec<&str> = students.iter()
.filter(|(_, score, subject)| *subject == "Math" && *score >= 90)
.map(|(name, _, _)| *name)
.collect();
top_math.sort();
println!("Top Math students: {:?}", top_math);
// Average score per subject
let subjects = ["Math", "Science"];
for subject in subjects {
let scores: Vec<u32> = students.iter()
.filter(|(_, _, s)| *s == subject)
.map(|(_, score, _)| *score)
.collect();
let avg = scores.iter().sum::<u32>() as f64 / scores.len() as f64;
println!("{} average: {:.1}", subject, avg);
}
// All students scoring above average
let overall_avg: f64 = students.iter().map(|(_, s, _)| *s as f64).sum::<f64>()
/ students.len() as f64;
println!("\nOverall average: {:.1}", overall_avg);
let above_avg: Vec<&str> = students.iter()
.filter(|(_, score, _)| *score as f64 > overall_avg)
.map(|(name, _, _)| *name)
.collect();
println!("Above average: {:?}", above_avg);
}
Output:
Top Math students: ["Alice", "Eve"]
Math average: 91.3
Science average: 62.7
Overall average: 77.0
Above average: ["Alice", "Carol", "Eve"]
Summary
In this post you learned:
- ✅ Closures are anonymous functions defined with
|params| body - ✅ Closures capture variables from their environment (borrow or move)
- ✅ The three closure traits:
Fn,FnMut,FnOnce - ✅ Iterators are lazy — they do nothing until consumed
- ✅ Adapter methods:
map,filter,filter_map,take,skip,chain,zip,flat_map,enumerate - ✅ Consuming methods:
sum,collect,count,find,fold,any,all - ✅ You can build custom iterators by implementing the
Iteratortrait
What's Next?
In the final post of this series, we'll bring everything together with an introduction to Modules, Packages, and Cargo — how to organize Rust code into files, modules, and crates, and how to use third-party libraries from crates.io.
Next Post: Modules, Packages, and Cargo in Rust →
Part of the Learn Rust from Scratch series on CodeWithNeo


Leave a Reply