
Efficient data processing and functional-style programming are at the heart of modern Rust development. In this post, we’ll explore two of Rust’s most powerful tools — iterators and closures — and show how they unlock clean, expressive, and performant code.
Introduction
When working with collections, transforming data, or writing elegant, expressive Rust code, iterators and closures are indispensable tools. Iterators allow you to process sequences without verbose loops, while closures let you encapsulate behavior in a concise, flexible way.
But behind their elegant surface lies a world of subtle power. Rust’s iterator system is zero-cost and deeply composable, with traits like Iterator
, IntoIterator
, and DoubleEndedIterator
enabling powerful combinations. Closures, meanwhile, capture environment variables by reference or by move, which interacts intricately with ownership, lifetimes, and borrowing.
In this post, we’ll demystify both iterators and closures. You’ll learn how they work, when to use them, and how to leverage them effectively in your Rust projects — including chaining, lazy evaluation, custom iterator types, and closure traits like Fn
, FnMut
, and FnOnce
.
By the end, you’ll be able to write expressive, efficient Rust code that’s idiomatic and powerful.
Understanding Iterators in Rust
Iterators are one of Rust’s most elegant and powerful abstractions. They provide a concise, expressive, and zero-cost way to process sequences of data. Whether you’re filtering a list, transforming values, or chaining multiple operations together, iterators are the idiomatic way to handle such patterns in Rust.
But they’re more than just syntactic sugar — Rust’s iterator system is deeply grounded in traits, strongly typed, and optimized at compile time. Understanding how iterators work under the hood will not only make you a more fluent Rust developer but also help you write more efficient and expressive code.
What is an Iterator?
An iterator is any type that implements the Iterator
trait. This trait defines how a sequence of values can be iterated over, one at a time, usually in a loop. The core method of the Iterator
trait is:
fn next(&mut self) -> Option<Self::Item>;
This means that when you call .next()
on an iterator, it returns either:
Some(item)
if there is a next item in the sequence, orNone
if the iterator has been exhausted.
At a high level, you can think of an iterator as something like a cursor that moves through a collection. Each call to next()
advances the cursor and gives you the next element, if there is one.
Let’s get started writing some code.
Open a shell window (Terminal on Mac/Linux, Command Prompt or PowerShell on Windows). Then navigate to the directory where you store Rust packages
for this blog series, and run the following command:
cargo new iterators_closures
Next, change into the newly created iterators_closures
directory and open it in VS Code (or your favorite IDE).
Note: Using VS Code is highly recommended for following along with this blog series. Be sure to install the Rust Analyzer extension — it offers powerful features like code completion, inline type hints, and quick fixes.
Also, make sure you’re opening the iterators_closures directory itself in VS Code. If you open a parent folder instead, the Rust Analyzer extension might not work properly — or at all.
As we see examples in this post, you can either replace the contents of main.rs or instead comment out the current code for future reference with a multi-line comment:
/*
CODE TO COMMENT OUT
*/
Now, open the file src/main.rs and replace its contents entirely with the code for this example.
Example: Iterating Over a Vector
fn main() {
let numbers = vec![10, 20, 30];
let mut iter = numbers.iter();
println!("First: {:?}", iter.next()); // Some(&10)
println!("Second: {:?}", iter.next()); // Some(&20)
println!("Third: {:?}", iter.next()); // Some(&30)
println!("Done: {:?}", iter.next()); // None
}
/* Output:
First: Some(10)
Second: Some(20)
Third: Some(30)
Done: None
*/
numbers.iter()
returns an iterator over references to the elements of the vector.- Calling
next()
advances the iterator and returns the next value, wrapped inSome(...)
. - Once the iterator is exhausted,
next()
returnsNone
.
Iterators Work with Loops Too
A for
loop in Rust is just syntactic sugar for repeatedly calling .next()
on an iterator behind the scenes:
fn main() {
let numbers = vec![100, 200, 300];
for number in numbers.iter() {
println!("Got: {}", number);
}
}
/* Output:
Got: 100
Got: 200
Got: 300
*/
Under the hood, Rust:
- Calls
.iter()
to create the iterator. - Calls
.next()
repeatedly. - Stops when
.next()
returnsNone
.
Key Points:
- All iterators must implement the
Iterator
trait. - Iterators are lazy: they do nothing until you consume them (via
for
,collect
,next()
, etc.). - They return items wrapped in
Option
, allowing the iteration to naturally end. - You can create iterators from many types, including arrays, vectors, ranges, and even custom types.
The Iterator
trait and its key methods
At the heart of Rust’s iteration system is the Iterator
trait. Any type that implements this trait can be iterated over using a for
loop, or used with the rich set of iterator adapters and consumers that Rust provides.
Understanding the core methods of this trait — especially next()
, but also methods like size_hint()
, nth()
, count()
, and last()
— will give you deep insight into how iteration works in Rust and allow you to take full control when needed.
Here’s a simplified view of the Iterator
trait from the standard library (no need to copy this code):
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// Many default methods provided here:
fn count(self) -> usize where Self: Sized { ... }
fn last(self) -> Option<Self::Item> where Self: Sized { ... }
fn nth(&mut self, n: usize) -> Option<Self::Item> { ... }
fn size_hint(&self) -> (usize, Option<usize>) { ... }
// ...and many others
}
Let’s break down the most commonly used methods and what they do.
next()
: The Core of Iteration
As seen earlier, next()
is how an iterator produces each item, one at a time:
fn main() {
let mut iter = [1, 2, 3].iter();
println!("{:?}", iter.next()); // Some(&1)
println!("{:?}", iter.next()); // Some(&2)
println!("{:?}", iter.next()); // Some(&3)
println!("{:?}", iter.next()); // None
}
/* Output:
Some(1)
Some(2)
Some(3)
None
*/
You must call it on a mutable reference to the iterator since it advances internal state.
let mut iter = [1, 2, 3].iter();
count()
: Consumes and Counts
This method consumes the iterator and returns the number of items left.
fn main() {
let v = vec![10, 20, 30, 40];
let count = v.iter().count();
println!("Total items: {}", count); // 4
}
/* Output:
Total items: 4
/*
Once count()
is called, the iterator is consumed — it can’t be used again unless recreated, and that’s by design.
If you want to get the count without consuming the iterator, you must:
- Clone the iterator (if possible), or
- Use
size_hint()
for an estimate, or - Collect into an intermediate collection and inspect that (not ideal for large data).
But in practice, it’s usually not a concern, because:
- Iterators are cheap to recreate (especially from slices or collections).
- Iterator chains are often consumed exactly once, by design.
- If you really need to reuse data, you can just keep the original collection and re-iterate.
last()
: Get the Last Element (also consumes the iterator)
Returns the last item produced by the iterator, or None
if it’s empty.
fn main() {
let v = vec![3, 5, 9];
let last = v.iter().last();
println!("Last item: {:?}", last); // Some(&9)
}
/* Output:
Last item: Some(9)
*/
nth(n)
: Skip to the N-th Element
Returns the n-th item (zero-based), consuming items along the way.
fn main() {
let v = vec![100, 200, 300, 400];
let third = v.iter().nth(2);
println!("Third item: {:?}", third);
}
/* Output:
Third item: Some(300)
*/
This method consumes items up to and including the n-th (zero-based) item in the iterator and returns it.
- It advances the iterator
n + 1
steps. - It consumes those
n + 1
items. - The iterator can still be used afterward, starting from the item after the one returned.
size_hint()
: Estimating Size
Returns a tuple, lower and upper bound on how many items the iterator will yield. Useful for optimization.
fn main() {
let v = vec![1, 2, 3];
let iter = v.iter();
let (lower, upper) = iter.size_hint();
println!("Lower: {}, Upper: {:?}", lower, upper); // Lower: 3, Upper: Some(3)
}
/* Output
Lower: 3, Upper: Some(3)
*/
This is especially helpful when writing your own custom iterators or designing performance-critical APIs.
.size_hint()
is a non-consuming operation. It simply inspects the internal state of the iterator and returns an estimate (or exact count) of how many elements remain. It does not call next()
, and it does not advance the iterator at all.
- Many iterator methods like
map
,filter
,take
, andskip
return new iterator adapters — they don’t consume the iterator immediately. - Methods like
count()
,last()
,nth()
, andcollect()
consume the iterator — meaning it can’t be reused afterward unless you recreate it.
The role of next()
, size_hint()
, and others
Rust’s iterator system centers around a few core methods that define how data is accessed, inspected, and consumed. Among them, next()
is the foundational method, while methods like size_hint()
, nth()
, count()
, and last()
build on top of it, either by repeatedly calling next()
internally or by informing consumers about what to expect.
Understanding the distinct roles these methods play — and how they work together — gives you deeper control over iteration behavior and helps you write more idiomatic and efficient Rust code.
next()
: The Foundation
- What it does: Returns the next item from the iterator, or
None
when done. - Mutates state: Must be called on
&mut self
. - Drives all other methods:
count()
,nth()
,last()
, and many adapters (likemap()
orfilter()
) rely on callingnext()
repeatedly.
fn main() {
let mut iter = vec![1, 2, 3].into_iter();
while let Some(val) = iter.next() {
println!("Got: {}", val);
}
}
/* Output:
Got: 1
Got: 2
Got: 3
*/
size_hint()
: Reporting Remaining Items
- What it does: Reports the estimated number of items remaining, as a
(min, max)
tuple. - Does not mutate state: You can call it without consuming the iterator.
- Used by adapters like
collect()
to pre-allocate memory when possible.
fn main() {
let v = vec![10, 20, 30];
let mut iter = v.iter();
println!("Initial hint: {:?}", iter.size_hint()); // (3, Some(3))
iter.next(); // consume one
println!("After one next(): {:?}", iter.size_hint()); // (2, Some(2))
}
/* Output:
Initial hint: (3, Some(3))
After one next(): (2, Some(2))
*/
nth(n)
: Skip and Retrieve
- What it does: Skips
n
items and returns then
-th, consuming up to that point. - Useful for skipping ahead in large iterators.
fn main() {
let mut iter = vec!['a', 'b', 'c', 'd'].into_iter();
let val = iter.nth(2); // Skip to index 2
println!("Third item: {:?}", val); // Some('c')
for x in iter {
println!("Left: {}", x); // Should print only 'd'
}
}
/* Output:
Third item: Some('c')
Left: d
*/
count()
: Consume and Count
- What it does: Counts how many items are left, consuming the entire iterator.
- Useful when you only care about the number of items.
fn main() {
let v = vec![100, 200, 300];
let count = v.iter().count(); // Consumes the iterator
println!("Count: {}", count);
}
/* Output:
Count: 3
*/
last()
: Return the Final Item
- What it does: Advances through the entire iterator and returns the last item.
- Also consumes the iterator.
fn main() {
let v = vec![5, 10, 15];
let last = v.iter().last(); // Consumes everything
println!("Last item: {:?}", last); // Some(&15)
}
/* Output:
Last item: Some(15)
*/
Method | Consumes? | Purpose | Notes |
---|---|---|---|
next() | ✅ Yes | Retrieve the next item | Core of iteration |
size_hint() | ❌ No | Report remaining items (min, max) | Helpful for pre-allocation |
nth(n) | ✅ Yes | Skip to the n-th item | Partial consumption |
count() | ✅ Yes | Count remaining items | Fully consumes |
last() | ✅ Yes | Return final item | Fully consumes |
How These Work Together
You can think of:
next()
as the engine,size_hint()
as the fuel gauge,- and methods like
count()
,nth()
, andlast()
as high-level consumers or single-use shortcuts built onnext()
.
These methods allow for both low-level control and high-level convenience, giving you the best of both worlds in Rust.
Consuming vs adapting iterator methods
Rust’s iterator API can be broadly divided into two powerful categories:
consuming methods and adapting methods.
- Consuming methods exhaust the iterator to produce a final value or effect (like
count
,collect
,find
). - Adapting methods return a new iterator that wraps and transforms the original one (like
map
,filter
,take
).
Understanding the difference is critical when building iterator chains — it determines when the iteration actually happens, and whether you can continue using an iterator afterward.
Consuming Methods
These methods take ownership of the iterator and use .next()
internally to consume items. Once a consuming method is called, the iterator is exhausted and can’t be reused.
Common consuming methods:
.collect()
.count()
.last()
.find()
.any()
,.all()
,.fold()
,.for_each()
Example: count()
consumes the iterator
fn main() {
let v = vec![1, 2, 3, 4];
let iter = v.iter();
let count = iter.count(); // Consumes the iterator
println!("Count: {}", count);
// Error if you try: iter.next(); — iterator is moved and gone
}
/* Output:
Count: 4
*/
Example: collect()
consumes and transforms into a new collection
fn main() {
let v = vec![1, 2, 3];
let iter = v.iter().map(|x| x * 2); // map is adapting
let doubled: Vec<_> = iter.collect(); // collect consumes
println!("Doubled: {:?}", doubled); // [2, 4, 6]
}
/* Output:
Doubled: [2, 4, 6]
*/
Adapting Methods
These methods don’t consume items immediately. Instead, they return a new iterator that wraps the original. These are lazy: nothing happens until a consuming method (like for
, collect
, or next
) is called.
Common adapting methods:
.map()
.filter()
.take()
,.skip()
.enumerate()
.chain()
,.zip()
Example: map()
creates a new iterator, doesn’t consume
fn main() {
let v = vec![10, 20, 30];
let mapped = v.iter().map(|x| x + 1); // lazy — nothing happens yet
// Now we consume it with for loop
for val in mapped {
println!("Mapped: {}", val);
}
}
/* Output:
Mapped: 11
Mapped: 21
Mapped: 31
*/
Example: Combining Adapters Before Consuming
fn main() {
let data = vec![1, 2, 3, 4, 5];
let result: Vec<_> = data
.iter()
.filter(|&&x| x % 2 == 0) // keep even numbers
.map(|x| x * 10) // multiply each by 10
.collect(); // now consume
println!("Filtered and mapped: {:?}", result); // [20, 40]
}
/* Output:
Filtered and mapped: [20, 40]
*/
Until .collect()
is called, no filtering or mapping actually happens. That’s the power of lazy evaluation.
Method Type | Does it consume the iterator? | Returns a new iterator? | Triggers execution? |
---|---|---|---|
map , filter | ❌ No | ✅ Yes | ❌ No (lazy) |
collect , count | ✅ Yes | ❌ No | ✅ Yes |
for loop | ✅ Yes | N/A | ✅ Yes |
next() | ✅ Yes (one item) | ❌ No | ✅ Yes |
Best Practice
Chain as many adapting methods as you want to build a processing pipeline, and finish with a consuming method to execute it.
This model:
- Keeps code clean and expressive
- Minimizes memory usage (thanks to laziness)
- Gives you total control over transformation and evaluation
Using Built-In Iterator Adapters
Rust iterators aren’t just for looping — they’re a powerful, lazy transformation pipeline.
Iterator adapters are methods that don’t immediately produce results. Instead, they return a new iterator that wraps the original one, modifying the behavior when it’s eventually consumed. These adapters enable you to write concise, expressive logic — filtering, mapping, skipping, enumerating — all without manually writing loops.
In this section, we’ll explore the most common built-in adapters. These are the building blocks of idiomatic Rust data processing and work beautifully with closures.
map
, filter
, take
, skip
, enumerate
, and more
Rust provides a rich set of iterator adapters that allow you to manipulate, filter, transform, or limit what comes out of an iterator — all while keeping your code lazy and efficient. Here we’ll walk through the most frequently used ones.
map
: Transforming Each Item
The map
adapter applies a closure to each item and returns a new iterator over the transformed items.
fn main() {
let numbers = vec![1, 2, 3];
let doubled: Vec<_> = numbers.iter().map(|x| x * 2).collect();
println!("Doubled: {:?}", doubled); // [2, 4, 6]
}
/* Output:
Doubled: [2, 4, 6]
*/
map
doesn’t run the transformation until you consume the iterator (withcollect
,for
, etc.).- The closure takes a reference when using
.iter()
and a value when using.into_iter()
.
A closure is like an anonymous function — a compact way to define behavior inline. You’ll learn all about closures later in this post, but for now, think of |x| x * 2
as a lightweight function that takes x
and returns x * 2
.
filter
: Keeping Items That Match a Condition
The filter
adapter keeps only items that satisfy a predicate (closure that returns true
).
fn main() {
let data = vec![1, 2, 3, 4, 5, 6];
let evens: Vec<_> = data.iter().filter(|&x| x % 2 == 0).collect();
println!("Even numbers: {:?}", evens); // [2, 4, 6]
}
/* Output:
Even numbers: [2, 4, 6]
*/
- Closure receives each item as a reference (
&i32
here). - Returns only the items for which the closure returns
true
.
Note that adapter methods receive a reference or a value depends entirely on the type of iterator you’re using — not the method itself.
Method | Item type in iteration |
---|---|
.iter() | &T (reference) |
.iter_mut() | &mut T (mutable reference) |
.into_iter() | T (owned value, if possible) |
In the following code, the closure to map() doesn’t seem to take a reference (x: &i32) but that’s because numbers contains primitive values, so Rust automatically deferences them.
let doubled: Vec<_> = numbers.iter().map(|x| x * 2).collect();
take(n)
: Limit to First N Items
The take
adapter limits the iteration to the first n
items.
fn main() {
let nums = vec![10, 20, 30, 40, 50];
let first_three: Vec<_> = nums.iter().take(3).collect();
println!("First 3: {:?}", first_three); // [10, 20, 30]
}
/* Output:
First 3: [10, 20, 30]
*/
skip(n)
: Skip the First N Items
The skip
adapter discards the first n
items and begins yielding after that.
fn main() {
let nums = vec![10, 20, 30, 40, 50];
let after_two: Vec<_> = nums.iter().skip(2).collect();
println!("After skipping 2: {:?}", after_two); // [30, 40, 50]
}
/* Output:
After skipping 2: [30, 40, 50]
*/
enumerate()
: Yields a tuple with the item index and value
The enumerate
adapter pairs each item with its index, starting from 0.
fn main() {
let fruits = vec!["apple", "banana", "cherry"];
for (i, fruit) in fruits.iter().enumerate() {
println!("{}: {}", i, fruit);
}
}
/* Output:
0: apple
1: banana
2: cherry
*/
- Great for cases where you need item position + value.
Adapter Combinations: Functional Pipelines
You can freely combine adapters into chains before consuming the iterator:
fn main() {
let result: Vec<_> = (1..=10)
.filter(|x| x % 2 == 0) // evens
.map(|x| x * x) // square
.take(3) // take first 3 results
.collect();
println!("Processed: {:?}", result); // [4, 16, 36]
}
/* Output:
Processed: [4, 16, 36]
*/
Adapter | Description | Consumes? |
---|---|---|
map | Apply transformation to each item | ❌ |
filter | Keep only items matching condition | ❌ |
take(n) | Limit output to first n items | ❌ |
skip(n) | Skip first n items | ❌ |
enumerate | Pair items with index | ❌ |
collect | Consume and gather into collection | ✅ |
Adapters are lazy and chainable, giving you efficient pipelines. Only when you consume the chain (e.g., with collect
, for
, or next
) does it execute.
Combining multiple adapters
Rust’s iterator adapters really shine when you start chaining them together. Because they are lazy and each adapter returns another iterator, you can compose pipelines of transformations and filters with minimal overhead and crystal-clear logic.
This chaining style is not only elegant — it’s idiomatic Rust. You build a pipeline of data transformations, and nothing happens until the final consuming method (like collect
, for
, or fold
) is called.
Let’s look at how to combine multiple adapters to build expressive, high-performance logic.
Example: Filter → Map → Collect
fn main() {
let data = vec![1, 2, 3, 4, 5, 6];
let result: Vec<_> = data
.iter()
.filter(|&x| x % 2 == 0) // Keep only even numbers
.map(|x| x * 10) // Multiply each by 10
.collect(); // Consume and collect into Vec
println!("Processed: {:?}", result);
}
/*
Output:
Processed: [20, 40, 60]
*/
- Each adapter returns a new iterator.
- Nothing is executed until
.collect()
is called. - This pipeline is concise, readable, and avoids temporary variables or extra loops.
Example: Skip → Take → Enumerate → Map
fn main() {
let animals = vec!["cat", "dog", "fox", "owl", "bat", "rat"];
let tagged: Vec<String> = animals
.iter()
.skip(2) // Skip the first two animals
.take(3) // Take the next three
.enumerate() // Attach an index (0, 1, 2)
.map(|(i, animal)| format!("{}: {}", i, animal))
.collect();
for line in tagged {
println!("{}", line);
}
}
/* Output:
0: fox
1: owl
2: bat
*/
This example combines four adapters:
skip(2)
skips itemstake(3)
limits how many to yieldenumerate()
attaches an indexmap()
transforms each(index, item)
pair into a string
Still lazy, still efficient, still readable.
Example: Processing and Summing Numbers Inline
fn main() {
let sum: i32 = (1..=10)
.map(|x| x * 2) // double each number
.filter(|x| x % 3 == 0) // keep divisible by 3
.sum(); // consume and compute total
println!("Total: {}", sum);
}
/* Output:
Total: 36
Explanation: doubled values = [2,4,6,8,10,12,14,16,18,20]; keep 6,12,18; sum = 36
*/
Here, instead of .collect()
, we use .sum()
— another consuming method.
- Each adapter (
map
,filter
,take
, etc.) returns a new iterator that wraps the previous one. - The final adapter is followed by a consuming method, which triggers the actual work.
- This means:
- You avoid unnecessary memory allocation.
- You process only what’s needed.
- Your code stays clean and composable.
Best Practices
- Use method chaining for clarity and performance.
- Keep each adapter’s closure small and focused.
- Add line breaks between each chained method for readability in longer pipelines.
- If readability suffers, extract closures into named functions.
Lazy evaluation and why it’s efficient
Rust iterators are lazy by design — they don’t perform any work until a consuming method (like .collect()
, .sum()
, .for_each()
, etc.) is called. This laziness is one of the biggest reasons why Rust’s iterator model is so powerful and efficient.
Laziness means:
- Intermediate operations like
map
,filter
, ortake
don’t run right away. - No intermediate memory allocations are made.
- Only as many items as needed are produced and processed.
- You can build efficient, fine-tuned pipelines that avoid wasteful computation.
Why This Matters
In many languages, chaining operations (like filtering, mapping, slicing) creates intermediate collections at each step — which can be slow and memory-hungry.
In Rust:
- No intermediate Vecs or arrays are created.
- Each item flows through the chain only when needed, one at a time.
- This enables tight, allocation-free loops — often inlined by the compiler.
Example: Nothing Happens Without a Consumer
fn main() {
let numbers = vec![1, 2, 3];
let _lazy_iter = numbers.iter().map(|x| {
println!("Mapping: {}", x);
x * 2
});
println!("Iterator created, but nothing printed yet.");
}
/* Output:
Iterator created, but nothing printed yet.
*/
Even though map()
includes a println!
, nothing is printed, because the iterator hasn’t been consumed.
Example: Work Happens Only When Consumed
fn main() {
let numbers = vec![1, 2, 3];
let result: Vec<_> = numbers.iter().map(|x| {
println!("Mapping: {}", x);
x * 2
}).collect();
println!("Result: {:?}", result);
}
/* Output:
Mapping: 1
Mapping: 2
Mapping: 3
Result: [2, 4, 6]
*/
Here, calling .collect()
triggers the map()
operation. Laziness ensures that we only do the work once, at the point of consumption.
Example: Laziness Stops Early When Possible
This shows that only needed elements are processed — not the entire iterator.
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let first_even = numbers
.iter()
.map(|x| {
println!("Doubling: {}", x);
x * 2
})
.find(|x| x % 2 == 0); // find the first even number
println!("Found: {:?}", first_even);
}
/* Output:
Doubling: 1
Found: Some(2)*/
Although the vector has 5 items, the iterator only processes one item because map() doubles 1 to 2, and so .find()
stops after the first match. No wasted computation.
Benefits of Laziness
- Performance: No intermediate allocations, no extra loops.
- Control: Work only happens when you decide to consume.
- Efficiency: Early-exit consumers like
find
,any
,take
,nth
minimize processing. - Composability: You can build long pipelines without paying for unused work.
Feature | Benefit |
---|---|
Lazy adapters | No work until needed |
No allocation | Memory-efficient, zero-cost abstraction |
Composability | Stackable, expressive pipelines |
Early exit | Skip unneeded work |
Consuming Iterators
Rust’s iterators don’t just loop over data — they transform, reduce, and act on it through powerful consuming methods. These methods are the final step of any lazy iterator chain. They take ownership of the iterator and drive execution.
Some consumers, like .collect()
and .fold()
, produce values. Others, like .for_each()
or .find()
, perform actions or stop early.
In this section, we’ll explore these common consumers:
.collect()
— gather items into a collection.fold()
— reduce items into a single value.for_each()
— run a closure for side effects.find()
— locate the first match and stop.any()
and.all()
— check conditions across items
You’ll see how to use them effectively and idiomatically in Rust.
collect()
, fold()
, for_each()
, find()
, etc.
Rust’s iterator consumers aren’t just about reaching the end — they’re about what you want to do with the data along the way.
- Do you want to build a collection from results? Use
.collect()
. - Need to sum, combine, or accumulate? Reach for
.fold()
. - Running side effects like
println!
on each item? That’s.for_each()
. - Looking for just one matching item?
.find()
or.any()
will do the job efficiently.
In this subsection, we’ll look at the most useful and expressive consuming methods — the ones that turn iterator pipelines into meaningful work. You’ll see how each one behaves, when it short-circuits, and what kind of result it returns.
Each of these methods takes your lazy pipeline and makes something happen — efficiently, clearly, and idiomatically.
collect()
: Gather Into a Collection
The .collect()
method transforms an iterator into a concrete collection like Vec
, HashMap
, or String
. It’s one of the most common consumers.
fn main() {
let nums = vec![1, 2, 3];
let doubled: Vec<_> = nums.iter().map(|x| x * 2).collect();
println!("Doubled: {:?}", doubled);
}
/* Output:
Doubled: [2, 4, 6]
*/
- The target collection is inferred by the left-hand side (or can be explicitly typed).
- You can collect into any type that implements the
FromIterator
trait (e.g.,Vec
,HashMap
,String
).
fold()
: Accumulate Into a Single Value
.fold()
reduces all items into one by applying a closure with an accumulator.
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let sum = numbers.iter().fold(0, |acc, x| acc + x);
println!("Sum: {}", sum);
}
/* Output:
Sum: 15
*/
- The first argument is the initial accumulator value.
- The closure takes
(accumulator, item)
and returns the updated accumulator.
for_each()
: Apply Side Effects
.for_each()
runs a closure on each item for its side effects. It returns ()
.
fn main() {
let names = vec!["Alice", "Bob", "Carol"];
names.iter().for_each(|name| {
println!("Hello, {}!", name);
});
}
/* Output:
Hello, Alice!
Hello, Bob!
Hello, Carol!
*/
Use .for_each()
when your goal is to do something with the items, rather than transform or collect them.
find()
: Stop at the First Match
.find()
searches for the first item that matches a condition, and then stops.
fn main() {
let words = vec!["apple", "banana", "grape"];
let first_with_a = words.iter().find(|&&word| word.contains('a'));
println!("First match: {:?}", first_with_a);
}
/* Output:
First match: Some("apple")
*/
- Short-circuits as soon as a match is found.
- Returns
Option<Item>
.
any()
and all()
: Boolean Conditions
These methods test conditions across all items and short-circuit as needed.
fn main() {
let nums = vec![1, 2, 3, 4];
let has_even = nums.iter().any(|&x| x % 2 == 0);
let all_less_than_10 = nums.iter().all(|&x| x < 10);
println!("Has even: {}", has_even); // true
println!("All < 10: {}", all_less_than_10); // true
}
/* Output:
Has even: true
All < 10: true
*/
.any()
returns true if any item matches..all()
returns true if all items match.
Both stop iterating as soon as the result is determined.
Consumer | Purpose | Returns | Short-circuits? |
---|---|---|---|
collect() | Turn iterator into a collection | Collection | ❌ |
fold() | Reduce to a single value | Final result | ❌ |
for_each() | Perform actions per item | () | ❌ |
find() | Return first match | Option<Item> | ✅ |
any() | Check if any item matches | bool | ✅ |
all() | Check if all items match | bool | ✅ |
Consuming vs borrowing: What happens to the iterator and the data?
When working with Rust iterators, it’s crucial to understand not just how you use them, but what ownership model you’re working with — especially when chaining methods or reusing values.
Sometimes your iterator is consumed (you can’t use it again), other times it just borrows data (and the original data stays accessible). These behaviors are consistent, but can be subtle if you’re not clear on how Rust handles ownership, borrowing, and lifetimes in iterators.
In this section, we’ll walk through exactly what happens to:
- The iterator itself: Is it still usable?
- The data it’s iterating over: Is it borrowed, moved, or still intact?
Example: Borrowing With .iter()
— Non-consuming and Reusable Data
fn main() {
let nums = vec![1, 2, 3];
// .iter() borrows the data (nums is not consumed)
let sum: i32 = nums.iter().sum(); // consumes the iterator, not the vector
println!("Sum: {}", sum);
println!("Original data still usable: {:?}", nums); // ✅ nums is still accessible
}
/* Output:
Sum: 6
Original data still usable: [1, 2, 3]
*/
.iter()
creates an iterator over references (&i32
), sonums
is not moved or consumed.- The iterator is consumed by
.sum()
, butnums
is still usable afterwards.
Example: Consuming With .into_iter()
— Moving the Data
fn main() {
let words = vec!["a".to_string(), "b".to_string(), "c".to_string()];
// .into_iter() takes ownership of each String
let collected: Vec<String> = words.into_iter().map(|s| s + "!").collect();
println!("Modified: {:?}", collected);
// println!("{:?}", words); // ❌ compile error: use of moved value
}
/* Output:
Modified: ["a!", "b!", "c!"]
*/
.into_iter()
takes ownership of the elements inwords
.- The original vector
words
is moved and no longer usable afterward. - The data is transferred into the iterator, then transformed and collected into a new vector.
Example: Borrowing the Iterator vs Moving It
fn main() {
let data = vec![1, 2, 3];
let mut iter = data.iter(); // iter borrows data
println!("{:?}", iter.next()); // Some(&1)
println!("{:?}", iter.next()); // Some(&2)
// The iterator is mutable, not moved — we can still use it
println!("{:?}", iter.next()); // Some(&3)
println!("{:?}", iter.next()); // None
}
/* Output:
Some(1)
Some(2)
Some(3)
None
*/
- The iterator is mutable, but it’s not consumed by calling
next()
. - You can continue using it as long as it’s not moved elsewhere.
Rules of Thumb
Scenario | What Happens |
---|---|
.iter() | Borrows the data (&T ) — original remains usable |
.iter_mut() | Mutable borrow (&mut T ) — data is still intact |
.into_iter() | Moves the data (T ) — original is consumed |
Calling .next() | Mutably borrows the iterator, advances it |
Calling .collect() / .sum() | Consumes the iterator (not the source unless moved) |
Example: Reusing Borrowed vs Moved Data
fn main() {
let names = vec!["Greg", "Alice", "Bob"];
// Use .iter(), so names is only borrowed
for name in names.iter() {
println!("Hello, {}", name);
}
println!("Original still intact: {:?}", names); // ✅
// Now consume it with .into_iter()
let uppercased: Vec<_> = names.into_iter().map(|n| n.to_uppercase()).collect();
// println!("{:?}", names); // ❌ Error: use of moved value
println!("Uppercased: {:?}", uppercased);
}
/* Output:
Hello, Greg
Hello, Alice
Hello, Bob
Original still intact: ["Greg", "Alice", "Bob"]
Uppercased: ["GREG", "ALICE", "BOB"]
*/
Method | Borrows or Moves? | Can you reuse the original? |
---|---|---|
.iter() | Borrows | ✅ Yes |
.iter_mut() | Mutable borrow | ✅ Yes (with &mut ) |
.into_iter() | Moves | ❌ No |
Consumer methods (collect , sum ) | Consume the iterator, not the data (unless moved in) | Depends |
Understanding whether you’re borrowing or moving — and whether the iterator itself is being consumed — helps avoid compile errors and gives you cleaner, more intentional code.
4. Creating Custom Iterators
While Rust gives you a rich toolbox of built-in iterators, sometimes you need full control — like iterating over a custom data structure or generating values on the fly. In these cases, you can create your own iterator by implementing the Iterator
trait.
Creating custom iterators is surprisingly straightforward — and a powerful way to encapsulate logic that would otherwise live in verbose loops or state machines.
In this section, you’ll learn how to define a struct that tracks iteration state and implements the Iterator
trait manually. This opens the door to expressive, reusable iteration patterns tailored to your specific needs.
Implementing the Iterator
trait
To create a custom iterator, you:
- Define a struct that holds the iteration state.
- Implement the
Iterator
trait for your struct. - Define the associated
Item
type and thenext()
method.
The next()
method is where the magic happens — it’s responsible for advancing and returning the next item (or None
when done).
Example: A Simple Counter Iterator
Let’s build an iterator that counts from a start value up to a limit.
struct Counter {
current: usize,
max: usize,
}
impl Counter {
fn new(max: usize) -> Self {
Counter { current: 0, max }
}
}
impl Iterator for Counter {
type Item = usize;
fn next(&mut self) -> Option<Self::Item> {
if self.current < self.max {
let result = self.current;
self.current += 1;
Some(result)
} else {
None
}
}
}
fn main() {
let counter = Counter::new(5);
for num in counter {
println!("Count: {}", num);
}
}
/*
Output:
Count: 0
Count: 1
Count: 2
Count: 3
Count: 4
*/
- The
Counter
struct holds state (current
andmax
). - The
Iterator
trait implementation defines:type Item = usize;
— the type produced on each iteration.next(&mut self)
— called each time the iterator advances.
- The loop calls
next()
repeatedly until it returnsNone
.
Example: Custom Iterator That Yields Fibonacci Numbers
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 + self.next;
let result = self.curr;
self.curr = self.next;
self.next = new_next;
Some(result)
}
}
fn main() {
let fib = Fibonacci::new();
for n in fib.take(10) {
println!("Fibonacci: {}", n);
}
}
/*
Output:
Fibonacci: 0
Fibonacci: 1
Fibonacci: 1
Fibonacci: 2
Fibonacci: 3
Fibonacci: 5
Fibonacci: 8
Fibonacci: 13
Fibonacci: 21
Fibonacci: 34
*/
- Implementing
Iterator
is just a matter of tracking state and definingnext()
. - This gives you complete control over iteration behavior.
- Your custom iterator integrates seamlessly with all the usual adapters and consumers (
map
,take
,collect
, etc.).
Real-world example: Building a custom range or sequence generator
Custom iterators are not just for exotic data structures — they’re incredibly useful when you want to generate a sequence of values with behavior that the standard library doesn’t quite cover.
Maybe you want:
- A range with a non-standard step (like 3 instead of 1)
- A descending sequence
- Skipped or custom-patterned values
- A bounded countdown timer
Let’s build a custom iterator that behaves like range
, but with a configurable step — something you can’t do with Rust’s built-in Range
types alone.
Example: A Range Iterator with a Step
This iterator yields numbers from start
up to (but not including) end
, advancing by a custom step value.
struct StepRange {
current: i32,
end: i32,
step: i32,
}
impl StepRange {
fn new(start: i32, end: i32, step: i32) -> Self {
assert!(step != 0, "step must not be zero");
StepRange { current: start, end, step }
}
}
impl Iterator for StepRange {
type Item = i32;
fn next(&mut self) -> Option<Self::Item> {
if (self.step > 0 && self.current >= self.end)
|| (self.step < 0 && self.current <= self.end)
{
return None;
}
let result = self.current;
self.current += self.step;
Some(result)
}
}
fn main() {
let forward = StepRange::new(0, 10, 2);
println!("Forward stepping by 2:");
for n in forward {
println!("{}", n);
}
let backward = StepRange::new(10, 0, -3);
println!("\nBackward stepping by -3:");
for n in backward {
println!("{}", n);
}
}
/*
Output:
Forward stepping by 2:
0
2
4
6
8
Backward stepping by -3:
10
7
4
1
*/
- You can add logic in
next()
to handle forward/backward logic. - This example supports any integer direction and ensures robust behavior.
- You now have a
range(start, end, step)
that behaves more like Python’srange()
— a great utility in Rust projects.
As an exercise, you could extend this example to:
- Include the end value (like
inclusive
) - Support
f32
/f64
steps - Allow filtering or mapping via composition
This real-world example demonstrates how creating a simple custom iterator:
- Solves a limitation in built-in types (
std::ops::Range
only steps by 1) - Stays ergonomic and idiomatic
- Leverages everything you’ve learned about
Iterator
,next()
, and lazy evaluation
Custom iterators give you expressive control without sacrificing performance — and they plug right into Rust’s existing iterator ecosystem.
When and why you might create your own iterator
With all the powerful iterator adapters and built-in methods available in Rust, you might wonder: When would I ever need to implement my own iterator?
In most day-to-day Rust code, you’ll be fine using .iter()
, .map()
, .filter()
, and other standard tools. But there are real scenarios where building your own iterator provides clarity, reusability, and control that no combination of built-in adapters can easily match.
Let’s explore when a custom iterator is the right tool — and how it can make your code more expressive and maintainable.
When to Create a Custom Iterator
Here are some common use cases:
1. You Need Stateful Iteration Logic
If your logic requires tracking internal state across iterations (e.g., current index, previous value, dynamic bounds), it’s often cleaner to encapsulate it in a struct and implement Iterator
.
2. You Want to Encapsulate Complex Logic
Rather than writing inline logic or managing temporary variables across a for
loop, a custom iterator can make code modular and self-contained.
3. You’re Building a Domain-Specific Sequence
Whether it’s Fibonacci numbers, prime generators, batched data, or paginated requests — a custom iterator gives you domain-friendly syntax and reusability.
4. You Want Composability
Once you implement Iterator
, your custom type can use .map()
, .filter()
, .take()
, .collect()
, and all the other adapter and consumer methods — just like built-in iterators.
Example: Paginated Iterator for API Pages
Imagine you’re calling a paginated API and want to iterate through all the pages until none are left. A custom iterator makes this logic clean and idiomatic:
struct PageFetcher {
current_page: usize,
total_pages: usize,
}
impl PageFetcher {
fn new(total_pages: usize) -> Self {
PageFetcher {
current_page: 1,
total_pages,
}
}
}
impl Iterator for PageFetcher {
type Item = String;
fn next(&mut self) -> Option<Self::Item> {
if self.current_page > self.total_pages {
return None;
}
// Simulate fetching a page
let result = format!("Fetched page {}", self.current_page);
self.current_page += 1;
Some(result)
}
}
fn main() {
let pages = PageFetcher::new(3);
for page in pages {
println!("{}", page);
}
}
/*
Output:
Fetched page 1
Fetched page 2
Fetched page 3
*/
This is much more expressive than managing a while
loop and manual state tracking.
Example: Alternating On/Off Sequence Generator
Another fun use case — generate a repeating on/off sequence (like a blinking LED simulation):
struct OnOff {
state: bool,
}
impl OnOff {
fn new() -> Self {
OnOff { state: false }
}
}
impl Iterator for OnOff {
type Item = &'static str;
fn next(&mut self) -> Option<Self::Item> {
self.state = !self.state;
Some(if self.state { "ON" } else { "OFF" })
}
}
fn main() {
let sequence = OnOff::new();
for signal in sequence.take(6) {
println!("{}", signal);
}
}
/*
Output:
ON
OFF
ON
OFF
ON
OFF
*/
This kind of pattern — toggling, rotating, or stateful output — is much easier to express cleanly with a custom iterator.
Summary: Why Create Your Own Iterator?
Reason | Benefit |
---|---|
Custom stateful logic | Cleanly encapsulates looping state |
Reusable domain-specific iteration | Keeps logic modular and testable |
Seamless integration with adapters | You can chain .map() , .take() , .collect() , etc. |
Cleaner and more expressive code | Avoids manual state tracking and nested loops |
You don’t need to write custom iterators often — but when you do, they can drastically improve the clarity, reusability, and power of your code.
Closures in Rust
Closures are at the heart of Rust’s functional-style programming — and they work hand-in-hand with iterators. You’ve already seen closures in action when using map
, filter
, and for_each
, but now it’s time to take a step back and look at what closures really are, how they differ from regular functions, and why they’re so central to expressive, idiomatic Rust.
A closure is a lightweight, anonymous function that you can define inline — usually by using |args| expression
syntax. But closures are more than just syntax sugar. They can capture variables from their surrounding environment, can be stored in variables, passed around like data, and even returned from functions.
In this section, we’ll start by understanding the syntax and anatomy of closures, and then dive into how they capture environment, how the compiler decides their traits (Fn
, FnMut
, FnOnce
), and how to use them effectively with iterators and beyond.
Syntax and anatomy of a closure
Closures in Rust use a distinctive syntax based on vertical bars (|
) to enclose the parameter list, and can optionally include a body block or a single-line expression.
Basic Form
let add_one = |x| x + 1;
This creates a closure that takes one parameter x
and returns x + 1
. You can immediately call it like this:
fn main() {
let add_one = |x| x + 1;
let result = add_one(5);
println!("Result: {}", result);
}
/*
Output:
Result: 6
*/
The Rust compiler infers the parameter and return types in many cases, just like with let
. But you can also annotate types explicitly:
let multiply = |x: i32, y: i32| -> i32 {
x * y
};
Closure vs Function: What’s the Difference?
Here’s a regular function and an equivalent closure:
fn add_fn(x: i32, y: i32) -> i32 {
x + y
}
let add_closure = |x: i32, y: i32| -> i32 {
x + y
};
Both do the same thing, but the closure is a value — it can be stored in a variable and passed around as a first-class citizen.
Closures are inline, often more concise, and can capture values from the surrounding scope — something functions cannot do.
Closures Can Capture Their Environment
Closures can access variables outside of their argument list. Here’s an example:
fn main() {
let factor = 3;
let multiply = |x| x * factor; // closure captures `factor`
println!("3 * 4 = {}", multiply(4));
}
/*
Output:
3 * 4 = 12
*/
The closure remembers the value of factor
— it’s not passed in, but it’s still used internally. This behavior is what allows closures to act like compact, contextual functions.
Block Body vs Expression Body
Closures can be a single-line expression, or a multi-line block just like functions:
let describe = |x: i32| {
println!("Value: {}", x);
if x > 0 { "positive" } else { "non-positive" }
};
The last expression in the block becomes the return value (unless you use return
, which is discouraged in closures unless breaking early).
Summary: Closure Anatomy
Feature | Description |
---|---|
|args| expr | Basic closure syntax |
Optional types | Type inference by default, but can be explicit |
Expression or block | Single-line or multi-line body |
Captures variables | Can access values from outer scope |
First-class values | Can be stored, passed, returned like data |
Closures feel like functions — but they behave more like function objects that carry state, context, and can be flexibly moved around in your code.
Capturing variables by reference, mutable reference, or move
One of the most powerful features of closures in Rust is their ability to capture variables from the surrounding environment. But how they do that — by reference, by mutable reference, or by move — depends on how the closure is used.
Rust automatically figures out how to capture each variable in the most efficient way, based on the operations you perform inside the closure. This behavior determines what kind of closure trait (Fn
, FnMut
, or FnOnce
) the closure implements.
Let’s break down how these capture modes work, and how to tell what’s happening under the hood.
Capturing by Reference (Fn
)
If the closure only reads from a variable (doesn’t mutate or move it), Rust captures it by shared reference (&T
), and the closure implements the Fn
trait.
fn main() {
let name = "Greg";
let greet = || {
println!("Hello, {}!", name); // captured by reference
};
greet();
greet(); // can be called multiple times — no mutation or move
}
/*
Output:
Hello, Greg!
Hello, Greg!
*/
name
is captured immutably.- Closure can be called many times.
- This is the most lightweight and common case.
Capturing by Mutable Reference (FnMut
)
If the closure modifies a captured variable, Rust captures it by mutable reference (&mut T
), and the closure implements the FnMut
trait.
fn main() {
let mut count = 0;
let mut increment = || {
count += 1; // captured by mutable reference
println!("Count is now: {}", count);
};
increment();
increment();
println!("Final count: {}", count);
}
/*
Output:
Count is now: 1
Count is now: 2
Final count: 2
*/
- The closure mutates
count
, so it needs&mut count
. - The closure must be marked
mut
to call it. - Still reusable, but now it mutably borrows the environment.
Capturing by Move (FnOnce
)
If the closure takes ownership of a captured value (e.g., moves it), then the closure implements the FnOnce
trait — it can only be called once, because it consumes what it captures.
fn main() {
let message = String::from("Goodbye");
let consume = || {
println!("Consumed: {}", message); // message is moved
};
consume(); // OK
// consume(); // ❌ Error: closure has moved `message`
}
/*
Output:
Consumed: Goodbye
*/
String
is notCopy
, so printing it moves it into the closure.- Closure consumes the value — it’s a one-time-use closure.
You can also force a move using the move
keyword:
fn main() {
let data = vec![1, 2, 3];
let consume_vec = move || {
println!("Moved vector: {:?}", data); // `data` is moved into the closure
};
consume_vec();
// println!("{:?}", data); // ❌ Error: `data` was moved
}
/*
Output:
Moved vector: [1, 2, 3]
*/
Use move
when the closure needs to own its captured values, such as when you’re passing it into a thread or returning it from a function.
Capture Mode | Triggered When… | Closure Trait |
---|---|---|
By reference | You read variables (no move or mutation) | Fn |
By mutable reference | You modify variables | FnMut |
By move | You take ownership (e.g., non-Copy types) | FnOnce |
Rust chooses the least restrictive mode that works based on what your closure does.
✅ Best Practices
- Start simple — let the compiler infer how to capture.
- If you need ownership (e.g., for threading), use
move
. - Use
mut
when a closure modifies state, just like variables.
Inference of closure traits: Fn
, FnMut
, FnOnce
In Rust, closures don’t require you to manually declare which trait (Fn
, FnMut
, or FnOnce
) they implement. Instead, the compiler infers it automatically, based on how the closure captures and uses its surrounding variables.
This inference system is one of Rust’s ergonomic strengths — it lets you write clean closures without worrying about implementation details until you need to pass them around or store them in structs, traits, or function parameters.
Let’s look at how Rust decides which trait a closure implements — and what each trait means in practice.
🔹 The Three Closure Traits Recap
Trait | What It Means | Can Be Called… |
---|---|---|
Fn | Captures by immutable reference | Multiple times |
FnMut | Captures by mutable reference | Multiple times |
FnOnce | Captures by move | Once |
Example : Inferred Fn
(Reads Only)
fn call_twice<F: Fn()>(f: F) {
f();
f();
}
fn main() {
let message = "Hello";
let print = || {
println!("{}", message); // captured by immutable reference
};
call_twice(print); // OK — closure implements Fn
}
/*
Output:
Hello
Hello
*/
- The closure only reads
message
, so it’sFn
. - It can be called multiple times.
Example: Inferred FnMut
(Modifies Captured Data)
fn call_twice_mut<F: FnMut()>(mut f: F) {
f();
f();
}
fn main() {
let mut count = 0;
let mut increment = || {
count += 1; // captured by mutable reference
println!("Count: {}", count);
};
call_twice_mut(increment); // OK — closure implements FnMut
}
/*
Output:
Count: 1
Count: 2
*/
- The closure mutates
count
, so it needs mutable access. - Compiler infers
FnMut
.
Example: Inferred FnOnce
(Consumes Captured Value)
fn call_once<F: FnOnce()>(f: F) {
f();
}
fn main() {
let s = String::from("Goodbye");
let consume = || {
println!("Consuming: {}", s); // s is moved into the closure
};
call_once(consume); // OK — closure implements FnOnce
}
/*
Output:
Consuming: Goodbye
*/
- The closure takes ownership of
s
, so it can only be called once. - The compiler infers
FnOnce
.
Closure Trait Inference Rules
Rust uses the least restrictive trait that fits the closure’s behavior. Here’s how to think about it:
- If the closure only reads variables → it implements
Fn
. - If it mutates, but doesn’t move → it implements
FnMut
. - If it moves/consumes values → it implements
FnOnce
.
You can always use a less restrictive bound if it fits:
- A closure that implements
Fn
can also be used whereFnMut
orFnOnce
is expected. - A
FnMut
can be used whereFnOnce
is expected. - But not vice versa.
Bonus: Compiler Enforces the Right Trait
Try relaxing the trait too far, and Rust will stop you:
fn call_twice<F: Fn()>(f: F) {
f();
f();
}
fn main() {
let s = String::from("once");
let consume = || {
println!("{}", s); // moves s
};
// call_twice(consume); // ❌ Error: closure only implements FnOnce
}
- The compiler prevents calling a
FnOnce
-only closure twice viaFn
.
Summary: Let the Compiler Do the Work
You rarely need to declare closure traits yourself — Rust infers the correct one automatically. But you do need to know which trait your closure implements when:
- Passing closures into functions
- Storing closures in structs or enums
- Boxing or trait-objecting closures (e.g.,
Box<dyn FnMut>
)
Closures are function-like but behave like stateful objects with built-in trait-based contracts.
Closures as Function Parameters
One of the most powerful features of closures in Rust is their ability to be passed around like data. Since closures implement one of the Fn
, FnMut
, or FnOnce
traits, you can pass them as arguments to functions, return them from functions, or store them in structs.
This makes closures perfect for abstracting logic — especially in iterator pipelines, where you can parameterize transformations and filters without resorting to verbose boilerplate.
In this section, we’ll explore how to pass closures into functions, with a focus on real-world patterns using iterator adapters.
Using closures with iterator adapters
You’ve already seen closures used inline with adapters like map
, filter
, and for_each
. In this subsection, we’ll go deeper: why they work, how you can parameterize them, and how to define functions that accept closures for these adapters.
Closures are perfect for iterator adapters because:
- They’re lightweight and inline
- They can capture context from surrounding scope
- They conform to the exact traits required by iterator methods (e.g.,
FnMut
formap
,FnOnce
forfind
)
Let’s look at a few clear examples.
Example: map()
with an Inline Closure
fn main() {
let nums = vec![1, 2, 3, 4, 5];
let doubled: Vec<_> = nums.iter().map(|x| x * 2).collect();
println!("Doubled: {:?}", doubled);
}
/*
Output:
Doubled: [2, 4, 6, 8, 10]
*/
map()
expects a closure that implementsFnMut(&T) -> U
- Here, the closure
|x| x * 2
takes a reference (&i32
) and returns a new value - This closure is inferred to be
FnMut
, but behaves likeFn
Example: Passing a Named Closure Into a Function
Let’s abstract part of a pipeline by passing in a closure:
fn apply_transformation<F>(data: &[i32], func: F) -> Vec<i32>
where
F: Fn(i32) -> i32,
{
data.iter().map(|&x| func(x)).collect()
}
fn main() {
let nums = vec![1, 2, 3];
let result = apply_transformation(&nums, |x| x * 3);
println!("Tripled: {:?}", result);
}
/*
Output:
Tripled: [3, 6, 9]
*/
func
is a generic parameter implementingFn(i32) -> i32
- We pass
|x| x * 3
intoapply_transformation
- This approach is great for reusable filtering, mapping, etc.
Example: Using Capturing Closures with Adapters
Closures can capture context and still work perfectly with iterator adapters:
fn main() {
let threshold = 10;
let data = vec![5, 10, 15, 20];
let filtered: Vec<_> = data
.iter()
.filter(|&&x| x > threshold)
.collect();
println!("Greater than {}: {:?}", threshold, filtered);
}
/*
Output:
Greater than 10: [15, 20]
*/
- The closure
|&&x| x > threshold
capturesthreshold
by reference - This works seamlessly with
.filter()
, which expectsFnMut(&T) -> bool
Example: filter() with a Named Predicate Function
You can use functions instead of closures, too:
fn is_even(n: &i32) -> bool {
n % 2 == 0
}
fn main() {
let nums = vec![1, 2, 3, 4];
let evens: Vec<_> = nums.into_iter().filter(is_even).collect();
println!("Evens: {:?}", evens);
}
/*
Output:
Evens: [2, 4]
*/
This works because function pointers in Rust automatically implement all three closure traits (Fn
, FnMut
, FnOnce
).
Adapter | Closure Trait Expected | Common Use |
---|---|---|
map() | FnMut(Self::Item) -> B | Transform items |
filter() | FnMut(&Self::Item) -> bool | Keep some items |
for_each() | FnMut(Self::Item) | Perform side effects |
find() | FnMut(&Self::Item) -> bool | Search for match |
Closures work beautifully with iterator adapters because they’re:
- Inferred automatically
- Capable of capturing context
- Lightweight, composable, and expressive
Passing closures into functions: syntax and flexibility
Closures are most powerful when you can parameterize behavior — passing logic into a function, much like you’d pass data. In Rust, closures are first-class citizens, and passing them into functions is both ergonomic and flexible.
Whether you’re customizing how an iterator filters, defining retry logic, or abstracting an algorithm, passing closures into functions lets you write reusable and declarative code. The key is understanding how to declare closure parameters in your function signatures and what closure traits (Fn
, FnMut
, FnOnce
) to use.
General Closure Parameter Syntax
fn my_func<F>(closure: F)
where
F: Fn(...) -> ...,
{
// use closure here
}
F
is a generic type that implements a closure trait.Fn
,FnMut
, orFnOnce
depending on what the closure does.- You can call
closure(arg)
inside the function.
Example: Passing a Basic Fn
Closure
fn apply_to_ten<F>(func: F) -> i32
where
F: Fn(i32) -> i32,
{
func(10)
}
fn main() {
let result = apply_to_ten(|x| x + 5);
println!("10 + 5 = {}", result);
}
/*
Output:
10 + 5 = 15
*/
- The closure
|x| x + 5
implementsFn(i32) -> i32
- It’s passed into
apply_to_ten
and invoked inside the function
Example: Passing a FnMut
Closure
fn call_twice<F>(mut action: F)
where
F: FnMut(),
{
action();
action();
}
fn main() {
let mut count = 0;
let mut increment = || {
count += 1;
println!("Count is now: {}", count);
};
call_twice(increment);
println!("Final count: {}", count);
}
/*
Output:
Count is now: 1
Count is now: 2
Final count: 2
*/
- The closure mutates
count
, so it must beFnMut
- The function takes the closure as
mut
to allow repeated calls
Example: Passing a FnOnce
Closure
fn consume_and_print<F>(f: F)
where
F: FnOnce(),
{
f(); // only allowed once
}
fn main() {
let message = String::from("Goodbye!");
let print_message = || {
println!("{}", message); // move occurs here
};
consume_and_print(print_message);
// println!("{}", message); // ❌ compile error: moved
}
/*
Output:
Goodbye!
*/
- The closure takes ownership of
message
, so it only implementsFnOnce
FnOnce
is the most permissive trait — you can pass any closure to it
Choosing the Right Closure Trait
Trait | Captures | Use When… |
---|---|---|
Fn | &T | Closure just reads from environment |
FnMut | &mut T | Closure modifies variables it captures |
FnOnce | T | Closure takes ownership of something |
If in doubt, use:
Fn
when you don’t need mutation or ownershipFnMut
when mutating state (e.g., counters, accumulators)FnOnce
when consuming data (e.g.,String
,Vec
, etc.)
Example: Higher-Order Function with a Closure Filter
fn filter_values<F>(data: &[i32], predicate: F) -> Vec<i32>
where
F: Fn(i32) -> bool,
{
data.iter()
.map(|&x| x)
.filter(|x| predicate(*x))
.collect()
}
fn main() {
let values = vec![3, 6, 9, 12];
let evens = filter_values(&values, |x| x % 2 == 0);
println!("Even values: {:?}", evens);
}
/*
Output:
Even values: [6, 12]
*/
- This abstracts
filter()
and takes anyFn(i32) -> bool
closure - The closure logic can vary without changing the function
Summary: Closure Parameters in Functions
Syntax | Description |
---|---|
F: Fn(...) -> ... | Non-mutating, non-consuming closures |
F: FnMut(...) -> ... | Mutating closures |
F: FnOnce(...) -> ... | One-time-use closures (consume captures) |
mut f: F in arguments | Needed when calling f() multiple times and f is FnMut |
By designing your functions to accept closures, you make them more flexible, reusable, and testable — enabling a powerful functional style without sacrificing Rust’s performance or safety.
Generic bounds with impl Fn
, FnMut
, FnOnce
, and Box<dyn Fn>
When you write functions that accept closures, you have two main ways to express the parameter type:
- Generic type parameters with a
where
clause, likeF: Fn(T) -> U
. impl Trait
syntax, likeimpl Fn(T) -> U
for concise single-use closures.- Trait objects like
Box<dyn Fn(T) -> U>
when you want to store or return dynamically dispatched closures.
Choosing between these depends on whether your closure:
- Needs to be stored long-term
- Should support dynamic dispatch
- Will be used inline and inferred
- Can be generic or must be boxed
Let’s walk through each approach and when to use it.
Generic Bound with Fn
, FnMut
, or FnOnce
This is the most flexible and performant option. You define a generic parameter (like F
) and use a where
clause to constrain it to implement a closure trait.
fn apply_twice<F>(func: F) -> i32
where
F: Fn(i32) -> i32,
{
func(5) + func(10)
}
fn main() {
let result = apply_twice(|x| x * 2);
println!("Result: {}", result);
}
/*
Output:
Result: 30
*/
- The closure is monomorphized at compile time — zero runtime overhead.
- Works well when you’re not storing the closure — just using it during the call.
Using impl Fn(...)
Syntax (Concise Generic)
If you’re writing a single-use function and don’t need to name the closure type, you can use impl Fn(...)
directly in the parameter list:
fn apply_once(func: impl Fn(i32) -> i32) -> i32 {
func(3)
}
fn main() {
let tripled = apply_once(|x| x * 3);
println!("Tripled: {}", tripled);
}
/*
Output:
Tripled: 9
*/
- This is syntactic sugar for a generic parameter: Rust rewrites it as
F: Fn(...)
. - Clean and concise when you don’t need the type
F
elsewhere.
Box<dyn Fn> — Trait Objects for Dynamic Dispatch
When you need to store a closure in a struct, return it from a function, or pass around dynamically typed logic, you’ll use a boxed trait object.
fn make_printer() -> Box<dyn Fn()> {
let message = String::from("Hello from the heap!");
Box::new(move || {
println!("{}", message);
})
}
fn main() {
let printer = make_printer();
printer();
}
/*
Output:
Hello from the heap!
*/
Box<dyn Fn()>
is a trait object — heap-allocated, dynamically dispatched.- Necessary when you can’t know the closure type at compile time (e.g. returning different closures based on a condition).
- The closure must be
'static
unless you use lifetime parameters, since it’s stored in the heap and can escape the current scope.
Example: Storing a Closure in a Struct with Box<dyn Fn()>
struct Button {
on_click: Box<dyn Fn()>,
}
impl Button {
fn click(&self) {
(self.on_click)();
}
}
fn main() {
let message = String::from("Button clicked!");
let button = Button {
on_click: Box::new(move || {
println!("{}", message);
}),
};
button.click();
}
/*
Output:
Button clicked!
*/
- Closures with captures (like
message
) must usemove
to be boxed. Box<dyn Fn()>
allows closures to be stored in structs or passed across threads.
Summary: Choosing the Right Closure Bound
Syntax | Use Case | Trait Used | Performance |
---|---|---|---|
F: Fn(...) | Generic functions | Static dispatch | Zero cost |
impl Fn(...) | Concise one-off generic parameters | Static dispatch | Zero cost |
Box<dyn Fn()> | Store in structs or return from functions | Dynamic dispatch | Small overhead |
Best Practices
- Use generic bounds (
F: Fn...
) when performance matters or the closure is used inline. - Use
Box<dyn Fn>
when you need storage or dynamic behavior — especially in trait objects or collections. - Prefer
Fn
unless your closure needs to mutate or move values — in that case, useFnMut
orFnOnce
.
Closures vs Functions: When to Use Each
Closures and functions in Rust may look similar — both define reusable logic and can take parameters. But under the hood, they behave quite differently. Closures can capture their environment and are often defined inline, while functions are standalone, static, and cannot capture surrounding state.
So when should you use a closure, and when is a function the better tool? The answer depends on your use case — and sometimes on performance.
This section breaks down the differences, starting with performance, then moving to ergonomics, readability, and idiomatic usage.
Performance considerations
Closures and functions are both highly optimized in Rust — but they have different compilation and runtime behaviors that can matter in performance-sensitive code.
Key Differences That Affect Performance
Feature | Closure | Function |
---|---|---|
Can capture environment | ✅ Yes | ❌ No |
Requires heap allocation? | ❌ (usually not) / ✅ when boxed | ❌ Never |
Compile-time monomorphization | ✅ Yes (generic Fn ) | ✅ Yes |
Dynamic dispatch | ✅ If using Box<dyn Fn> | ✅ If using fn() pointers |
Zero-cost abstraction? | ✅ When passed as generic Fn | ✅ Always |
In practice:
- Inline closures with
Fn
,FnMut
, orFnOnce
traits are zero-cost — compiled like regular functions. - Boxed closures (
Box<dyn Fn>
) do have overhead due to heap allocation and dynamic dispatch. - Function pointers (
fn(i32) -> i32
) are always statically allocated and are very cheap to call.
Example: Using a Closure vs a Function
fn double_fn(x: i32) -> i32 {
x * 2
}
fn main() {
let double_closure = |x| x * 2;
let input = 5;
// Both return 10
println!("Function: {}", double_fn(input));
println!("Closure: {}", double_closure(input));
}
/*
Output:
Function: 10
Closure: 10
*/
At runtime, there’s no measurable performance difference here — both are statically dispatched and inlined by the compiler.
Example: Boxed Closure vs Function Pointer
fn call_with_fn_ptr(f: fn(i32) -> i32, x: i32) -> i32 {
f(x)
}
fn call_with_boxed_closure(f: Box<dyn Fn(i32) -> i32>, x: i32) -> i32 {
f(x)
}
fn double(x: i32) -> i32 {
x * 2
}
fn main() {
let boxed = Box::new(|x| x * 2);
println!("Function pointer: {}", call_with_fn_ptr(double, 4));
println!("Boxed closure: {}", call_with_boxed_closure(boxed, 4));
}
/*
Output:
Function pointer: 8
Boxed closure: 8
*/
fn
is a function pointer — no allocation, statically dispatched.Box<dyn Fn>
requires a heap allocation and dynamic dispatch, which is slightly slower.
You’d only use Box<dyn Fn>
when:
- The closure must be stored (e.g., in a struct)
- You don’t know the concrete type at compile time
- You need to return different closures from a function
Performance Best Practices
Scenario | Best Choice |
---|---|
Simple inline logic | Closure (Fn ) |
No environment capture, max speed | Function pointer (fn ) |
Storing closures long-term or dynamically | Box<dyn Fn> |
Inside iterator adapters | Closures (idiomatic, fast) |
Constant functions, reused broadly | Function (defined fn ) |
Closures are optimized aggressively by the compiler, especially when used as generics. In hot paths, prefer closures passed generically (impl Fn(...)
) over boxed trait objects unless dynamic behavior is truly needed.
Readability and flexibility
While closures and functions can often be used interchangeably, the readability and flexibility they provide vary depending on context.
Closures shine when you want inline behavior, minimal boilerplate, and contextual logic close to where it’s used. Functions excel when logic is reused, named, or requires documentation — especially when the closure grows beyond a few lines.
This sub section compares both in realistic scenarios to help you decide which approach improves clarity and maintainability.
Example: Readable Closure for Localized Logic
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<_> = numbers.iter().map(|x| x * 2).collect();
println!("Doubled: {:?}", doubled);
}
/*
Output:
Doubled: [2, 4, 6, 8, 10]
*/
- This is concise, expressive, and easy to read.
map
with a closure is idiomatic for single-purpose transformations.
Example: Named Function for Shared or Complex Logic
fn is_prime(n: &i32) -> bool {
if *n < 2 {
return false;
}
for i in 2..*n {
if n % i == 0 {
return false;
}
}
true
}
fn main() {
let nums = vec![3, 4, 5, 6, 7];
// destructure &&i32 to i32
let primes: Vec<_> = nums.iter().filter(|&&x| is_prime(&x)).collect();
println!("Primes: {:?}", primes);
}
/*
Output:
Primes: [3, 5, 7]
*/
is_prime
is complex enough to deserve its own name.- This improves testability, reusability, and documentation potential.
Example: Use a Closure When You Need Access to Outer Variables
fn main() {
let limit = 10;
let nums = vec![1, 5, 12, 8, 20];
let filtered: Vec<_> = nums
.iter()
.filter(|&&x| x < limit)
.collect();
println!("Filtered: {:?}", filtered);
}
/*
Output:
Filtered: [1, 5, 8]
*/
- Closures can capture
limit
from the environment without needing to pass it in. - This makes closures great for context-aware filters or transformations.
Example: Using a Function with Arguments for Reuse
fn is_under(n: &i32, limit: i32) -> bool {
*n < limit
}
fn main() {
let limit = 10;
let nums = vec![1, 5, 12, 8, 20];
// Use a closure to pass `limit` into the function
let filtered: Vec<_> = nums
.iter()
.filter(|&x| is_under(x, limit))
.collect();
println!("Filtered: {:?}", filtered);
}
/*
Output:
Filtered: [1, 5, 8]
*/
- Using a function is fine here, but you need a closure wrapper to pass extra context.
- Closures are cleaner if you only need to use the logic once and you’re capturing one variable.
Best Practices
Use a Closure When… | Use a Function When… |
---|---|
Logic is short, local, and not reused | Logic is complex, reused, or deserves a name |
You need to capture values from the environment | You want a standalone, testable unit of behavior |
You’re working with iterator adapters | You’re calling logic from multiple places |
You want maximum conciseness | You want maximum clarity and modularity |
Capturing state vs pure logic
One of the key differences between closures and functions in Rust lies in how they interact with external state.
- Closures can capture variables from their surrounding scope, making them ideal for behavior that’s tied to context — configuration, thresholds, counters, etc.
- Functions, by contrast, are pure logic: they can only use what’s explicitly passed in.
Understanding when to leverage captured state vs keeping logic pure and parameterized helps you write clearer, more intentional code.
Let’s compare both styles in real-world examples.
Example: Capturing State with a Closure
fn main() {
let threshold = 10;
let values = vec![3, 7, 15, 2, 20];
let filtered: Vec<_> = values.iter().filter(|&&x| x < threshold).collect();
println!("Below {}: {:?}", threshold, filtered);
}
/*
Output:
Below 10: [3, 7, 2]
*/
- The closure captures
threshold
by reference. - This is idiomatic, clean, and doesn’t require adding extra parameters.
Example: Pure Function with Parameters Instead of Captures
fn is_below(x: &i32, limit: i32) -> bool {
*x < limit
}
fn main() {
let threshold = 10;
let values = vec![3, 7, 15, 2, 20];
let filtered: Vec<_> = values
.iter()
.filter(|&x| is_below(x, threshold))
.collect();
println!("Below {}: {:?}", threshold, filtered);
}
/*
Output:
Below 10: [3, 7, 2]
*/
- The function is pure — it doesn’t rely on external variables.
- Makes it easier to test in isolation.
- Slightly more verbose to use when a single variable needs passing.
Example: Capturing a Mutable Counter with a Closure
Closures can also mutate captured state, which functions cannot do without explicit mutable references.
fn main() {
let mut count = 0;
let values = vec![1, 2, 3];
values.iter().for_each(|&x| {
count += x;
println!("Running total: {}", count);
});
println!("Final total: {}", count);
}
/*
Output:
Running total: 1
Running total: 3
Running total: 6
Final total: 6
*/
- The closure captures
count
by mutable reference. - This kind of stateful tracking is cleaner with closures than with external mutable references passed to functions.
Example: Pure Function Requires External State Passing
To do the same thing with a function:
fn add_to_total(x: i32, total: &mut i32) {
*total += x;
}
fn main() {
let mut total = 0;
let values = vec![1, 2, 3];
for x in &values {
add_to_total(*x, &mut total);
println!("Running total: {}", total);
}
println!("Final total: {}", total);
}
/*
Output:
Running total: 1
Running total: 3
Running total: 6
Final total: 6
*/
- This is still fine, but less ergonomic than a closure when used inline.
- Functions work best when logic is pure and independent of context.
Summary: Capturing State vs Pure Logic
Approach | Behavior | Best Used When… |
---|---|---|
Closure (capture) | Can read/mutate external variables | Logic depends on context (threshold, counter) |
Function (pure) | Needs everything passed as arguments | Logic is general-purpose and testable |
Best Practices
- Use closures when:
- Logic depends on context (e.g., a limit or setting)
- You want concise, inline behavior with no extra plumbing
- You need to track state (e.g., counters) in a loop
- Use functions when:
- Logic is independent of context
- You want to write unit tests
- You reuse the logic in multiple places
Closures give you contextual power, while functions give you explicit clarity. Choosing between them is about tradeoffs between convenience and purity.
Advanced Iterators + Closures in Practice
By now, you’ve seen how closures and iterators work individually — and together — to build expressive, functional-style Rust code. But where this model truly shines is in chaining multiple iterator adapters together to create powerful transformation pipelines.
In this section, we’ll apply what you’ve learned to real-world scenarios where iterator chains, closures, and lazy evaluation combine to produce clear, performant logic — often in just a few lines of code.
Chaining iterators for complex transformations
Chaining iterator adapters allows you to:
- Build step-by-step data transformations in a single expression
- Avoid temporary variables and manual loops
- Keep logic declarative, modular, and lazy
By combining adapters like filter
, map
, enumerate
, skip
, take_while
, and fold
, you can replace complex procedural logic with elegant iterator pipelines.
Let’s look at a few real-world inspired examples.
Example: Clean + Filter + Transform + Collect
fn main() {
let raw = vec![
"42", " 17", "not a number", "100", "", "33", " 7 ",
];
let parsed: Vec<i32> = raw
.iter()
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.filter_map(|s| s.parse::<i32>().ok())
.map(|n| n * 2)
.collect();
println!("Parsed and doubled: {:?}", parsed);
}
/*
Output:
Parsed and doubled: [84, 34, 200, 66, 14]
*/
map(trim)
cleans up whitespacefilter(!empty)
removes blanksfilter_map(parse)
converts strings to integers, skipping bad onesmap(*2)
transforms the valid data
This entire pipeline processes and transforms the data lazily and efficiently.
Example: Combine enumerate
, filter
, and map
for Indexed Filtering
fn main() {
let data = vec![10, 15, 20, 25, 30];
let even_indexed_doubled: Vec<_> = data
.iter()
.enumerate()
.filter(|(i, _)| i % 2 == 0) // keep even indices
.map(|(_, &val)| val * 2)
.collect();
println!("Even-indexed doubled values: {:?}", even_indexed_doubled);
}
/*
Output:
Even-indexed doubled values: [20, 40, 60]
*/
Example: Fold to Accumulate with Logic
fn main() {
let scores = vec![100, 85, 70, 50, 95];
let (passed, total): (Vec<_>, i32) = scores
.iter()
.fold((Vec::new(), 0), |(mut list, sum), &score| {
if score >= 70 {
list.push(score);
}
(list, sum + score)
});
println!("Passed: {:?}", passed);
println!("Total score: {}", total);
}
/*
Output:
Passed: [100, 85, 70, 95]
Total score: 400
*/
fold
gives you full control over accumulation.- You can return and update multiple values at once.
Chaining Tips
Adapter | Purpose |
---|---|
map() | Transform each item |
filter() | Keep items conditionally |
filter_map() | Filter + convert (e.g., parse strings) |
enumerate() | Track index along with values |
skip() , take() | Truncate input |
fold() | Accumulate into custom values |
Iterator chains read like pipelines — ideal for complex transformations without imperative loops or mutation.
✅ Best Practices
- Break long chains over multiple lines for readability.
- Use closures for inline logic; use named functions for complexity.
- Use
filter_map()
instead offilter().map()
when dealing withOption
output. - Prefer pipelines over manual loops for transformations and reductions.
Using closures to create inline logic
One of the most powerful aspects of closures in Rust is their ability to act as inline behavior. Instead of defining separate functions or managing verbose control flow, you can drop closures directly into your logic wherever behavior is needed.
Closures let you:
- Keep logic local to where it’s used
- Capture values from the surrounding context
- Pass behavior as first-class data
- Quickly prototype or customize without boilerplate
Let’s walk through some real-world patterns where inline closures make your code shorter, clearer, and more expressive.
Example: Using Closures in .map()
for Quick Transforms
fn main() {
let nums = vec![1, 2, 3, 4];
let squared: Vec<_> = nums.iter().map(|x| x * x).collect();
println!("Squared: {:?}", squared);
}
/*
Output:
Squared: [1, 4, 9, 16]
*/
- No need to define a
square()
function — the transformation is inline and readable. - This is especially useful for short, one-off logic.
Example: Capturing Context for Filtering
fn main() {
let limit = 10;
let values = vec![4, 8, 12, 16];
let below_limit: Vec<_> = values
.iter()
.filter(|&&x| x < limit)
.collect();
println!("Below {}: {:?}", limit, below_limit);
}
/*
Output:
Below 10: [4, 8]
*/
- The closure captures
limit
directly from its environment — no need to pass it around. - This is clean and avoids cluttering function signatures.
Example: Inline Decision Logic in .fold()
fn main() {
let words = vec!["apple", "banana", "cherry", "date"];
let longest = words.iter().fold("", |longest, &word| {
if word.len() > longest.len() {
word
} else {
longest
}
});
println!("Longest word: {}", longest);
}
/*
Output:
Longest word: banana
*/
- Instead of defining a separate function for comparison, the closure handles it inline.
- This keeps your accumulator logic self-contained and readable.
Example: Inline Closure to Sort by a Custom Rule
fn main() {
let mut data = vec!["abc", "z", "hello", "rust"];
data.sort_by_key(|s| s.len()); // sort by string length
println!("Sorted by length: {:?}", data);
}
/*
Output:
Sorted by length: ["z", "abc", "rust", "hello"]
*/
- Using a closure inside
.sort_by_key()
makes the sort behavior declarative and tight. - This kind of inline customization is hard to beat in clarity.
Why Use Inline Closures?
Benefit | Description |
---|---|
Conciseness | Keep logic close to where it’s used |
Contextual access | Capture thresholds, state, or configuration |
No name needed | Skip naming or defining unnecessary one-off functions |
Functional composition | Perfect fit for map , filter , fold , etc. |
Test-friendly | Easy to replace with function pointers or mocks if needed |
✅ When to Use Inline Closures
Use them when:
- The logic is short and self-contained
- You want to avoid jumping between definitions
- The closure captures local configuration or thresholds
- You’re working in a pipeline or higher-order function
Avoid them when:
- The logic gets too long or complex
- You’ll need the behavior in multiple places
- You want to unit test or document the logic separately
Real-world example: processing structured data with iterator chains
Rust’s iterator and closure system really shines when you’re working with structured data — like records, structs, or JSON-like values. Instead of verbose loops and nested conditionals, you can compose clear and functional pipelines to transform, filter, and summarize data.
Let’s walk through a typical use case: processing a list of user records to filter, extract, and compute information — all using iterator chains and closures.
Example: Filter Active Users, Extract Emails, and Count
Let’s say we have a User
struct and a collection of users:
#[derive(Debug)]
struct User {
id: u32,
email: String,
active: bool,
}
fn main() {
let users = vec![
User { id: 1, email: "alice@example.com".into(), active: true },
User { id: 2, email: "bob@example.com".into(), active: false },
User { id: 3, email: "carol@example.com".into(), active: true },
User { id: 4, email: "dan@example.com".into(), active: false },
User { id: 5, email: "eve@example.com".into(), active: true },
];
let active_emails: Vec<String> = users
.iter()
.filter(|user| user.active) // keep only active users
.map(|user| user.email.clone()) // extract their email
.collect();
println!("Active user emails: {:?}", active_emails);
let active_count = active_emails.len();
println!("Total active users: {}", active_count);
}
/*
Output:
Active user emails: ["alice@example.com", "carol@example.com", "eve@example.com"]
Total active users: 3
*/
What’s Happening
.iter()
gives us immutable references toUser
.filter()
uses a closure to check theactive
flag.map()
clones the email so we can collect into aVec<String>
.collect()
gathers the transformed data into a concrete type
This approach is:
- Concise — no temporary variables or manual looping
- Lazy — nothing happens until
.collect()
is called - Composable — each step is a focused, declarative transformation
Bonus: Group Users by Activity (with fold()
)
Let’s go further and group users into active and inactive buckets:
#[derive(Debug)]
struct User {
id: u32,
email: String,
active: bool,
}
fn main() {
let users = vec![
User { id: 1, email: "alice@example.com".into(), active: true },
User { id: 2, email: "bob@example.com".into(), active: false },
User { id: 3, email: "carol@example.com".into(), active: true },
User { id: 4, email: "dan@example.com".into(), active: false },
User { id: 5, email: "eve@example.com".into(), active: true },
];
let (active, inactive): (Vec<_>, Vec<_>) = users
.into_iter()
.fold((Vec::new(), Vec::new()), |(mut a, mut i), user| {
if user.active {
a.push(user.email);
} else {
i.push(user.email);
}
(a, i)
});
println!("Active: {:?}", active);
println!("Inactive: {:?}", inactive);
}
/*
Output:
Active: ["alice@example.com", "carol@example.com", "eve@example.com"]
Inactive: ["bob@example.com", "dan@example.com"]
*/
fold()
gives us complete control over grouping logic.- The accumulator holds two
Vec
s: one for active and one for inactive. - We push into the appropriate one based on the user’s status.
This example shows how iterator chains:
- Work seamlessly with custom structs
- Replace procedural logic with declarative transformations
- Scale beautifully for real-world tasks like filtering, mapping, and grouping
Once you master chaining closures with adapters like .filter()
, .map()
, and .fold()
, you can process almost any data structure in a clean, functional style.
Common Pitfalls and Borrow Checker Warnings
Closures and iterators in Rust are elegant and expressive — until you run into the borrow checker. Many common pitfalls happen when closures capture variables in unexpected ways, causing lifetime or mutability conflicts.
This section explores common problems and how to solve them, especially around ownership, borrowing, mutation, and move semantics when closures are involved.
We’ll begin with the most common issue: closure capture surprises.
Closure capture issues
Rust closures are smart — they automatically decide how to capture values from the environment based on usage: by reference, mutable reference, or move. But that same magic can lead to surprising errors if you’re not aware of what’s being captured and how.
Let’s look at what can go wrong, and how to fix it.
Example: Immutable Borrow Blocks Later Use
fn main() {
let list = vec![1, 2, 3];
let print = || {
println!("List: {:?}", list); // Captures `list` by reference
};
print();
// We try to mutate `list` after closure may have captured it
// list.push(4); // ❌ Error: cannot borrow `list` as mutable because it's already borrowed
println!("Done.");
}
/*
Output:
List: [1, 2, 3]
Done.
*/
Why it happens:
- The closure captured
list
by immutable reference. - Even though the closure is already used (
print()
), Rust assumes it may be called again later — solist
stays immutably borrowed.
Fix: Move the closure out of the way or drop it:
fn main() {
let mut list = vec![1, 2, 3];
{
let print = || {
println!("List: {:?}", list);
};
print(); // Closure is dropped here
}
list.push(4); // ✅ Now we can mutate
println!("Updated list: {:?}", list);
}
/*
Output:
List: [1, 2, 3]
Updated list: [1, 2, 3, 4]
*/
Example: Capturing by Move Prevents Reuse
fn main() {
let s = String::from("hello");
let consume = || {
println!("Using: {}", s); // `s` is moved into the closure
};
consume();
// consume(); // ❌ Error: closure has moved value
// println!("{}", s); // ❌ Error: `s` was moved
}
/*
Output:
Using: hello
*/
Why it happens:
s
is notCopy
, and it’s used (not just borrowed) inside the closure → Rust captures it by move.- Once the closure is called, the captured value is consumed.
Fixes:
- Clone
s
before use if needed multiple times - Use a reference if ownership isn’t needed:
fn main() {
let s = String::from("hello");
let print = || {
println!("Ref: {}", &s); // Now captured by reference
};
print();
print(); // ✅ Works multiple times
}
/*
Output:
Ref: hello
Ref: hello
*/
Example: Mutable Capture Conflicts
fn main() {
let mut count = 0;
let mut inc = || {
count += 1; // captured by mutable reference
};
inc();
inc();
// println!("{}", count); // ✅ Works
// let borrow = &count; // ❌ Error if placed before `inc()` is done mutably borrowing
}
Key point: When a closure mutably captures a variable, no other references (even immutable) to that variable are allowed at the same time.
Summary: Closure Capture Warnings
Problem | Root Cause | Fix |
---|---|---|
“cannot borrow X as mutable…” | Closure still holds a reference | Scope or drop the closure before re-borrowing |
“value moved into closure…” | Captured by move due to ownership use | Use clone() , & , or drop early |
“closure may outlive borrowed value…” | Lifetime inference or move vs borrow conflict | Add lifetimes or change capture strategy |
Best Practices
- If you see “moved into closure”, check whether you’re calling
.clone()
or capturing by reference instead. - If you get a “borrow later used” error, try scoping the closure inside a smaller block.
- Use
move
closures intentionally, especially when passing to threads or storing.
Lifetime and borrowing mistakes in closures and iterators
When closures and iterators involve borrowing, it’s easy to run into lifetime or borrowing issues — especially when the compiler can’t figure out how long references need to live, or when mutable and immutable borrows conflict.
Unlike simple loops, iterator chains and closures often extend borrow scopes implicitly, which can trip you up when you try to:
- Return references from closures
- Capture references in closures used beyond their original scope
- Mutably borrow a value that’s already immutably borrowed by an iterator
Let’s walk through common mistakes and how to resolve them cleanly.
Example: Returning a Reference from a Closure Without a Lifetime
fn get_first<'a>(words: &'a [String]) -> impl Fn() -> &'a String {
move || &words[0]
}
fn main() {
let list = vec!["hello".to_string(), "world".to_string()];
let first = get_first(&list);
println!("First word: {}", first());
}
/*
Output:
First word: hello
*/
This works because we explicitly annotated 'a
— the lifetime of the borrowed reference from words
.
But if we forget the lifetime:
// ❌ This won't compile:
fn get_first(words: &[String]) -> impl Fn() -> &String {
move || &words[0]
}
Rust complains:
missing lifetime specifier
✅ Fix: Add explicit lifetimes so the compiler knows the reference returned by the closure lives at least as long as words
.
Example: Borrowing While Iterating Then Mutating Later
fn main() {
let mut names = vec!["Greg", "Alice", "Bob"];
let lengths: Vec<_> = names.iter().map(|name| name.len()).collect();
// names.push("Carol"); // ❌ Error: cannot borrow `names` as mutable
println!("Lengths: {:?}", lengths);
}
/*
Output:
Lengths: [4, 5, 3]
*/
Even though we already collected the iterator, the compiler may still treat names
as borrowed until after the last use of the iterator chain.
Fix: Drop the borrow scope explicitly:
fn main() {
let mut names = vec!["Greg", "Alice", "Bob"];
let lengths: Vec<_> = {
names.iter().map(|name| name.len()).collect()
};
names.push("Carol"); // ✅ Works
println!("Updated names: {:?}", names);
println!("Lengths: {:?}", lengths);
}
/*
Output:
Updated names: ["Greg", "Alice", "Bob", "Carol"]
Lengths: [4, 5, 3]
*/
This forces the borrow created by .iter()
to end before we mutate names
.
Example 3: Returning an Iterator that Yields References
fn filtered<'a>(data: &'a Vec<i32>) -> impl Iterator<Item = &'a i32> {
data.iter().filter(|&&x| x > 10)
}
fn main() {
let list = vec![5, 10, 15, 20];
let results: Vec<_> = filtered(&list).collect();
println!("Filtered: {:?}", results);
}
/*
Output:
Filtered: [15, 20]
*/
Without the <'a>
annotation on the return type, Rust will complain:
cannot infer an appropriate lifetime
That’s because Rust needs to know that the references returned by the iterator are tied to the lifetime of the input Vec<i32>
.
Rule: When returning iterators or closures that yield references, always annotate the lifetimes explicitly.
Common Triggers of Lifetime and Borrow Errors
Mistake | Cause | Fix |
---|---|---|
Returning references without lifetimes | Rust can’t infer the relationship between inputs | Add lifetime annotations to function and return type |
Mutating after iteration | Borrow scope hasn’t ended | Wrap iteration in a {} block to force drop |
Mixing mutable and immutable borrows | Overlapping borrows (e.g. iter() vs push() ) | Separate scopes, or clone values if needed |
Using closures to return references | Closure’s lifetime must match borrowed data | Annotate with 'a or return owned values (String ) |
Best Practices
- Use lifetimes when your closure or iterator yields references.
- When in doubt, return owned values (e.g., use
.cloned()
or.to_owned()
). - Use
{}
blocks to explicitly limit borrow scope during iteration. - If a closure must return a reference, match its lifetime to the input source.
Debugging iterator chains
Iterator chains are concise, powerful, and expressive — but that elegance can backfire when you’re debugging. Because everything happens lazily and in one expression, it’s not always obvious:
- Where a transformation went wrong
- Whether a closure is being called
- How many elements are making it through each stage
Let’s look at practical techniques for diagnosing problems in iterator chains — from adding debug output, to splitting chains for clarity, to using .inspect()
and dbg!()
.
Example: Add .inspect()
to Peek Inside a Chain
Rust’s .inspect()
adapter allows you to “tap into” an iterator chain and run a side-effecting closure — great for debugging:
fn main() {
let nums = vec![1, 2, 3, 4, 5];
let result: Vec<_> = nums
.iter()
.inspect(|x| println!("Before filter: {}", x))
.filter(|&&x| x % 2 == 0)
.inspect(|x| println!("After filter: {}", x))
.map(|x| x * 10)
.inspect(|x| println!("After map: {}", x))
.collect();
println!("Final result: {:?}", result);
}
/*
Output:
Before filter: 1
Before filter: 2
After filter: 2
After map: 20
Before filter: 3
Before filter: 4
After filter: 4
After map: 40
Before filter: 5
Final result: [20, 40]
*/
.inspect()
prints intermediate values without affecting the chain.- Great for debugging logic step-by-step.
Example: Use dbg!()
to Debug Individual Steps
The dbg!()
macro prints both the value and the line number where it’s called:
fn main() {
let values = vec!["42", "abc", "99", ""];
let parsed: Vec<i32> = values
.iter()
.map(|s| s.trim())
.inspect(|s| {
dbg!(s);
})
.filter_map(|s| s.parse::<i32>().ok())
.collect();
println!("Parsed integers: {:?}", parsed);
}
/*
Output:
[src/main.rs:8:13] s = "42"
[src/main.rs:8:13] s = "abc"
[src/main.rs:8:13] s = "99"
[src/main.rs:8:13] s = ""
Parsed integers: [42, 99]
*/
dbg!()
is perfect for dropping into any closure.- It prints both value and source file/line info for fast debugging.
Example: Split Long Chains to Isolate Errors
Instead of writing a huge one-liner, split into named steps for clarity:
fn main() {
let nums = vec![1, 2, 3, 4, 5];
let step1 = nums.iter();
let step2 = step1.filter(|&&x| x > 2);
let step3 = step2.map(|x| x * 10);
let result: Vec<_> = step3.collect();
println!("Result: {:?}", result);
}
/*
Output:
Result: [30, 40, 50]
*/
- You can now insert breakpoints,
dbg!()
, orinspect()
between stages. - This is a powerful trick when debugging logic inside closures.
Debugging Techniques Cheat Sheet
Technique | Use Case |
---|---|
.inspect() | Peek inside a chain without breaking it |
dbg!() | Print value + location inside closures |
Stepwise chaining | Assign intermediate steps to variables |
collect() early | Break chain early to inspect values |
Add tests | Validate each transformation independently |
Best Practices for Debugging Iterators
- Use
.inspect()
anddbg!()
liberally during development — remove later. - If logic gets messy, extract closures into named functions to isolate behavior.
- If types get murky, annotate them with
let x: Type = ...
to see what the compiler expects. - Use
cargo expand
to see how your closure and iterator chains are being desugared (great for advanced users).
Wrapping Up: Mastering Iterators and Closures in Rust
Closures and iterators are two of Rust’s most powerful and expressive features — and when used together, they unlock an elegant, functional style of programming that’s both readable and performant.
In this post, you’ve learned not just how to use iterators and closures, but how to understand them deeply:
- You saw how iterators work under the hood — including adapters vs consumers, lazy evaluation, and custom implementations.
- You explored the anatomy of closures — their syntax, capture behavior, and the
Fn
,FnMut
, andFnOnce
traits. - You built practical, expressive pipelines with iterator chains, folding, and filtering.
- You navigated real-world use cases with structured data, explored lifetime issues, and learned how to debug complex iterator expressions.
- And you tackled common pitfalls with the borrow checker, closure captures, and type inference.
The Payoff
Rust’s iterator and closure systems are not just tools — they’re core to writing clean, modern Rust code. Whether you’re working with streams of data, transforming collections, or modeling custom iteration behavior, these features let you:
- Write less code that does more
- Eliminate boilerplate loops
- Avoid unnecessary allocation
- Express your intent clearly and precisely
Thanks for stopping by and allowing ByteMagma to be a part of your journey toward Rust programming mastery!
Leave a Reply