Filtering a Vec in Rust
You have a Vec<LogEntry> with 500 items. The dashboard only needs the errors. You could write a for loop, check a boolean flag, and push matching items to a new vector. That works. It is also verbose, prone to off-by-one mistakes, and fights the borrow checker if you try to optimize. Rust gives you a cleaner path. You chain filter and collect on an iterator. The code reads like the requirement. The compiler guarantees you process every element exactly once.
The concept: selection by predicate
Filtering is selection. You have a sequence of items. You apply a rule. Items that satisfy the rule continue. Items that fail drop out. The result is a new collection containing only the survivors.
Think of a quality control station on an assembly line. The conveyor belt carries parts. An inspector checks each part against a spec sheet. If the part passes, it moves to the packaging box. If it fails, it goes to the scrap bin. The inspector is your closure. The spec sheet is the condition. The packaging box is the resulting vector.
In Rust, the sequence is an iterator. The inspector is a closure that takes a reference to an item and returns a bool. true keeps the item. false discards it. The filter method wraps the iterator with this logic. The collect method at the end consumes the filtered stream and builds the final collection.
The critical detail is ownership. The iterator decides whether you get references or owned values. That choice determines whether you can move data out of the vector or only peek at it.
Minimal example: the basic chain
Start with a vector of integers. You want to keep only the even numbers.
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6];
// Move ownership into the iterator.
let evens: Vec<i32> = numbers
.into_iter()
.filter(|n| n % 2 == 0) // Keep if remainder is zero.
.collect(); // Build the new vector.
println!("{:?}", evens); // [2, 4, 6]
}
The into_iter call moves the vector into the iterator. You can no longer use numbers after this line. The iterator yields owned i32 values. The filter closure receives a reference to each value. The modulo check returns true for even numbers. The collect call consumes the filtered iterator and allocates a new Vec<i32>.
Type annotation on the variable is the community convention. The compiler needs to know what type collect produces. You can write collect::<Vec<i32>>(), but the annotation on the left side keeps the chain readable. It signals the intent clearly.
Write the intent, not the mechanics. The chain tells the reader exactly what happens.
How the chain executes
Iterators are lazy. Calling filter does not run the code. It builds a plan. The filter method returns a new iterator that wraps the original one. Nothing happens until you consume the iterator.
When collect runs, it pulls items one by one. For each item, it asks the filter iterator for the next value. The filter iterator pulls from the source, calls your closure, and decides whether to forward the item. If the closure returns true, the item passes through to collect. If false, the item drops immediately. There is no temporary list of rejected items.
The collect method allocates memory for the result. Since filter cannot predict how many items will pass, collect may reallocate as the vector grows. If you know the approximate size, you can use Vec::with_capacity and a loop, but for most cases, the default allocation strategy is fast enough.
Lazy evaluation is a feature. Consume the iterator or it does nothing.
Realistic example: structs and borrowing
Real code rarely filters plain integers. You usually filter structs. You might need to extract a field, handle missing data, or borrow the original collection.
#[derive(Debug)]
struct User {
name: String,
role: String,
email: Option<String>,
}
/// Extracts emails for active admins.
fn get_admin_emails(users: &[User]) -> Vec<String> {
users
.iter() // Borrow references to users.
.filter(|u| u.role == "admin") // Keep admins only.
.filter_map(|u| u.email.clone()) // Extract email, skip None.
.collect() // Gather owned strings.
}
fn main() {
let users = vec![
User { name: "Alice".into(), role: "admin".into(), email: Some("alice@example.com".into()) },
User { name: "Bob".into(), role: "user".into(), email: None },
User { name: "Charlie".into(), role: "admin".into(), email: Some("charlie@example.com".into()) },
];
let emails = get_admin_emails(&users);
println!("{:?}", emails); // ["alice@example.com", "charlie@example.com"]
}
The function takes a slice &[User]. Using iter borrows each user. You cannot move the User out of the slice. The filter checks the role. The filter_map handles two jobs at once. It extracts the email field. If the email is None, filter_map drops the item. If it is Some, filter_map unwraps it and passes the value forward. The clone moves the string out of the Option.
filter_map is the standard tool when you need to filter and transform in one step. It avoids a second pass over the data. Clippy will suggest filter_map if you chain filter and map together.
Combine filters and maps when the data shape changes.
Pitfalls and compiler errors
A common mistake is mixing borrowing and moving. If you use iter, you get references. You cannot move data out of the vector. If you try to collect owned values from references without cloning, the compiler stops you.
fn main() {
let words = vec!["hello".to_string(), "world".to_string()];
// This fails.
let result: Vec<String> = words
.iter()
.filter(|w| w.len() > 3)
.collect(); // Error: expected String, found &String.
}
The compiler rejects this with E0308 (mismatched types). The iterator yields &String. The closure borrows &&String. The result of collect would be Vec<&String>, but you asked for Vec<String>. You need to clone the strings or use into_iter.
// Fix: clone the strings.
let result: Vec<String> = words
.iter()
.filter(|w| w.len() > 3)
.map(|w| w.clone()) // Clone to move ownership.
.collect();
Another trap is forgetting that iterators are lazy. If you write the chain but never call collect, the filter never runs. You will get a warning about an unused variable, but the logic will not execute.
fn main() {
let data = vec![1, 2, 3];
// This does nothing.
let _filtered = data.iter().filter(|x| *x > 1);
// Warning: unused `Filter` that must be used.
}
The compiler warns you with a "unused Filter" message. Trust the warning. An unused iterator is a dead plan.
Borrowing rules also apply to captured variables. If your closure captures a mutable reference, the borrow checker enforces the usual rules. You cannot mutate a value while it is borrowed immutably elsewhere.
fn main() {
let mut counter = 0;
let data = vec![1, 2, 3];
// This fails.
let _result = data
.iter()
.filter(|_| {
counter += 1; // Error: cannot borrow counter as mutable.
true
})
.collect();
}
The compiler rejects this with E0596 (cannot borrow as mutable). The filter closure borrows data immutably. You cannot mutate counter inside the closure if counter is captured in a way that conflicts with the borrow. Use RefCell or restructure the code if you need mutation inside the chain.
The compiler error is a map. Read the code, not just the message.
Decision: when to use filter vs alternatives
Use filter when you want to keep a subset of items and build a new collection. The source data stays intact. The result is a fresh vector.
Use filter_map when you need to filter and transform in one step, or when the transformation might fail and return None. It avoids intermediate allocations and keeps the chain tight.
Use retain when you want to modify the vector in place, removing items that do not match. This avoids allocating a new vector. The vector shrinks, and the remaining items stay in memory.
Use partition when you need to split the collection into two groups based on the same condition, like separating errors from successes. It returns a tuple of two vectors.
Use a for loop with if when the logic is complex, requires early breaks, or involves side effects that do not fit the iterator pattern. Loops are more flexible but more verbose.
Pick the tool that matches your memory layout. In-place mutation saves memory. New collections preserve the original data.