Skip to Content
Course content

111: Chaining Iterators

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 HashSet to handle the uniqueness requirement before collecting into the final Vec.
  • Avoid using any manual for loops.
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)
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.