Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
111: Chaining Iterators
If you're coming from a language like JavaScript or Python, you might be looking at a long chain of .filter().map().take() and feeling a bit of performance anxiety. I see this all the time with developers moving to Rust: they assume that every time they call a method in a chain, Rust is allocating a new temporary collection in memory to hold the intermediate results before passing them to the next step.
"Each step in the chain creates a new list"
// The "Fearful" mental model:
let numbers = vec![1, 2, 3, 4, 5];
let result: Vec<_> = numbers.into_iter()
.filter(|x| x % 2 == 0) // "Creating a temporary list of [2, 4]?"
.map(|x| x * 2) // "Creating another temporary list of [4, 8]?"
.collect(); // "Finally putting it in the final list."
If that were true, chaining iterators would be a memory nightmare. But here is the reality: nothing happens until you call a consuming adapter like .collect() or a for loop. Those intermediate calls—filter, map, take—don't actually "do" anything to the data yet. They just wrap the previous iterator in a new struct that says, "When someone eventually asks me for a value, I'll ask the inner iterator for one, check if it passes the filter, and then transform it."
Laziness: The Pipeline Pattern
Think of an iterator chain not as a series of steps, but as a pipeline construction project. When you call .filter(), you aren't filtering data; you're just adding a "filter valve" to the pipe. When you call .map(), you're adding a "transformer" to the pipe. The water (your data) doesn't actually start flowing until you turn the tap at the very end with a method like .collect(), .sum(), or .fold().
I personally prefer this approach over manual for loops because it forces you to be declarative. You describe what you want to happen to the data, not how to manage the indices or temporary buffers. Plus, the Rust compiler is incredibly good at "inlining" these chains. In many cases, the generated assembly is identical to a single, tight while loop.
Let's look at a more realistic example. Imagine we have a list of store transactions and we only want the total value of "completed" orders that are over $100, applying a 10% loyalty discount to them.
struct Transaction {
id: u32,
amount: f64,
status: String,
}
fn main() {
let txs = vec![
Transaction { id: 1, amount: 150.0, status: "completed".to_string() },
Transaction { id: 2, amount: 50.0, status: "completed".to_string() },
Transaction { id: 3, amount: 200.0, status: "pending".to_string() },
Transaction { id: 4, amount: 300.0, status: "completed".to_string() },
];
let total_loyalty_value: f64 = txs.iter()
.filter(|t| t.status == "completed") // Step 1: Only completed
.filter(|t| t.amount > 100.0) // Step 2: Only high value
.map(|t| t.amount * 0.9) // Step 3: Apply 10% discount
.sum(); // Step 4: The "Tap" - actually execute everything
println!("Total: {}", total_loyalty_value);
}
In this snippet, the Transaction structs never move, and no intermediate vectors are created. The code simply pulls one transaction through the entire pipeline, then the next, and so on. It's efficient, clean, and avoids the "off-by-one" errors that plague manual looping.
📋 Practical Task
Exercise: Processing a Server Log for Error Analysis
You have a list of log entries. Your goal is to extract a list of unique error messages, but only for logs that are marked as "ERROR" and occur after a certain timestamp (represented as a simple integer). You must return these unique messages in a Vec<String>.
Requirements:
- Use a chain of iterator methods (
filter,map, etc.). - Use a
HashSetto handle the uniqueness requirement before collecting into the finalVec. - Avoid using any manual
forloops.
use std::collections::HashSet;
struct LogEntry {
timestamp: u64,
level: String,
message: String,
}
fn get_unique_recent_errors(logs: Vec<LogEntry>, min_timestamp: u64) -> Vec<String> {
// Your code here
}
fn main() {
let logs = vec![
LogEntry { timestamp: 100, level: "INFO".to_string(), message: "System boot".to_string() },
LogEntry { timestamp: 110, level: "ERROR".to_string(), message: "Timeout".to_string() },
LogEntry { timestamp: 120, level: "ERROR".to_string(), message: "Connection Lost".to_string() },
LogEntry { timestamp: 130, level: "ERROR".to_string(), message: "Timeout".to_string() }, // Duplicate
LogEntry { timestamp: 140, level: "INFO".to_string(), message: "Retry success".to_string() },
LogEntry { timestamp: 50, level: "ERROR".to_string(), message: "Old error".to_string() }, // Too old
];
let errors = get_unique_recent_errors(logs, 100);
println!("{:?}", errors);
// Expected: ["Timeout", "Connection Lost"] (order may vary)
}
There are no comments for now.