Improving our I/O project
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
- Change
mainsoConfig::build(env::args())compiles (nocollectinto aVecfirst). - Rewrite
Config::buildto takemut args: impl Iterator<Item = String>and pull query / path withnext(skip program name; noclone). - Rewrite
searchaslines().filter(...).collect()and keep the existing unit tests green. - Say aloud who owns each
Stringafterbuildreturns. - 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.