Hedronite Lesson · Polyglot-Dev / Rust · Wed 2026-09-23

Improving our I/O project

Own the args iterator. Drop the clones. Filter lines with adapters.

Lesson Class: Duha (Rust language track)
Focus: Config::build owns env::args · drop clone · search filter/collect · ownership
Code Blocks: clean blocks, explanation in prose
Done-criteria: Config::build owns Args iterator + search via adapters
Grounding: TRPL stable ch13-03 (+ ch13-00) · ch13-04 deferred · Cargo Topics #16 unmarked
The move
Pass env::args into build; stop cloning slice fields.
The pull
next discards the program name, then owns query and path.
The chain
lines().filter(...).collect() replaces the mutable push loop.
Own the args iterator. Drop the clones. Filter lines with adapters.

<!-- hal:authoritative:yaml -->

Own the args iterator. Drop the clones. Filter lines with an adapter chain. Keep ownership clear.

§I - Frame

Duha session 16. Closures (session 14) and iterators (session 15) already shipped. Today you apply them to the Chapter 12 minigrep project. Do not re-teach Fn traits or map/filter from scratch. Do not open ch13-04 performance beyond a short deferral note.

Syllabus note: operator RUST-SYLLABUS Topics #16 is still Cargo / Ch.14. This fire is an teammate-kick ch13-03 interstitial. Cargo stays unmarked.

Done-criteria: Can rewrite Config::build to own an Args iterator (no clone) and rewrite search with iterator adapters.

§II - Why clone was there

In Chapter 12, main collected env::args() into a Vec<String> and passed a slice into Config::build:

fn build(args: &[String]) -> Result<Config, &'static str> {
    if args.len() < 3 {
        return Err("not enough arguments");
    }
    let query = args[1].clone();
    let file_path = args[2].clone();
    let ignore_case = env::var("IGNORE_CASE").is_ok();
    Ok(Config { query, file_path, ignore_case })
}

build borrowed the slice, so it could not move String values out. clone bought ownership for Config at the cost of two allocations. TRPL deferred removing those clones until iterators were in hand. That time is now.

§III - Pass ownership of env::args

env::args already returns an iterator of String. Skip the intermediate vector. Pass the iterator into build:

fn main() {
    let config = Config::build(env::args()).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {err}");
        process::exit(1);
    });
    // --snip--
}

Update the signature so args is any iterator yielding String, and mark it mut because you will call next:

fn build(
    mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
    args.next(); // program name

    let query = match args.next() {
        Some(arg) => arg,
        None => return Err("Didn't get a query string"),
    };

    let file_path = match args.next() {
        Some(arg) => arg,
        None => return Err("Didn't get a file path"),
    };

    let ignore_case = env::var("IGNORE_CASE").is_ok();

    Ok(Config {
        query,
        file_path,
        ignore_case,
    })
}

The first next discards the program name. The next two move owned String values into Config. No indexing. No clone. Ownership is clear: the iterator gives up each String once, and Config holds them.

§IV - search with filter and collect

The Chapter 12 loop pushed into a mutable vector:

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();
    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }
    results
}

Rewrite with adapters. The closure captures query by reference (session 14 capture rules; session 15 filter):

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    contents
        .lines()
        .filter(|line| line.contains(query))
        .collect()
}

lines yields &str slices into contents. filter keeps matches. collect finishes the walk into Vec<&str>. No hand-built mutable results. The same pattern applies to search_case_insensitive if you lower the query once, then filter with a capturing closure.

Optional further step from the book: drop collect, return impl Iterator<Item = &'a str>, and let run's for print lazily. Keep that as a follow-on only after the Done-criteria two moves compile and pass tests. Performance comparison (ch13-04) stays deferred.

You can mirror the case-insensitive path the same way. Lower the query once, then capture it:

pub fn search_case_insensitive<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    let query = query.to_lowercase();
    contents
        .lines()
        .filter(|line| line.to_lowercase().contains(&query))
        .collect()
}

The owned query lives for the whole call. The closure borrows it. That is the same capture pattern from session 14, now seated inside an adapter chain from session 15. Prefer this shape over rebuilding a mutable results vector by hand.

§V - One complete proof

  1. Change main so Config::build(env::args()) compiles (no collect into a Vec first).
  2. Rewrite Config::build to take mut args: impl Iterator<Item = String> and pull query / path with next (skip program name; no clone).
  3. Rewrite search as lines().filter(...).collect() and keep the existing unit tests green.
  4. Say aloud who owns each String after build returns.
  5. Name why Topics #16 Cargo remains next and unmarked after this interstitial.

When those five hold, session 16's selected depth is done.

§VI - Closing

Iterators and closures earn their keep when they shrink real project code. Own the args iterator so Config takes String values by move. Filter lines instead of pushing into a mutable buffer. Cargo workspaces wait for the next Topics row; this interstitial only finishes the Ch.13 I/O rewrite.

Done-criteria: Can rewrite Config::build to own an Args iterator (no clone) and rewrite search with iterator adapters.

Related