From ba2884744bd13a6fafd717fa4377d90d98f635b5 Mon Sep 17 00:00:00 2001 From: darkicewolf50 Date: Mon, 24 Feb 2025 15:35:01 -0700 Subject: [PATCH] almost done with ch13.3 --- Improving The IO Project.md | 204 ++++++++++++++++++++++++++++++++++++ Iterators and Closures.md | 2 +- minigrep/src/lib.rs | 66 +++++++++--- minigrep/src/main.rs | 19 ++-- 4 files changed, 267 insertions(+), 24 deletions(-) create mode 100644 Improving The IO Project.md diff --git a/Improving The IO Project.md b/Improving The IO Project.md new file mode 100644 index 0000000..a2b09cb --- /dev/null +++ b/Improving The IO Project.md @@ -0,0 +1,204 @@ +# Improving the I/O Project +Using this new info about iterators we can improve the minigrep project by using iterators to make places in the code clearer and more concise + +Lets see how iterators can improve our implementation of the `Config::build` function and the `search` function + +## Removing a `clone` Using an Iterator +In before, we added code that took a slice of `String` values and created an instance of the `Config` struct by indexing into the slice and cloning the values, allowing the `Config` struct to own those values. + +Here we have reporduced the implementation fo te `Config::build` function as it was at the end of ch12 +```rust +impl Config { + pub fn build(args: &[String]) -> Result { + 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, + }) + } +} +``` +Before we said to not worry about the inefficient `clone` calls because we would remove them later. + +Now we will fix that + +We needed `clone` here because we have a slice with `String` elements in the parameter `args`, but the `build` function doesn't own `args` + +To return ownership of a `Config` instance we has to clone the values from the `query` and `file_path` fields of `Config` so the `Config` instance can own its values. + +Now with iterators we can chang the `build` function to take ownership of an iterator as its argument instad of borrowing a slice. + +We will use the iterator functionality instead of the code that checks the length of the slice and indexes into specific locations. + +This will clarify wat the `Config::build` function is doing because the iterator will access the values. + +Once `Config::build` takes ownership of the iterator and stops using indexing operations that borrow. + +We can then move the `String` values from the iterator into `Config` rather than calling `clone` and making a new allocation + +### Using the Returned Iterator Directly +The main.rs should look like this +```rust +fn main() { + let args: Vec = env::args().collect(); + + let config = Config::build(&args).unwrap_or_else(|err| { + eprintln!("Problem parsing arguments: {err}"); + process::exit(1); + }); + + // --snip-- +} +``` + +First we will change the start of `main` to use an iterator instead. + +This won't compile until we update `Config::build` as well +```rust +fn main() { + let config = Config::build(env::args()).unwrap_or_else(|err| { + eprintln!("Problem parsing arguments: {err}"); + process::exit(1); + }); + + // --snip-- +} +``` +The `env::args` fnction reutrns an iterator. + +Rather than collecting the iterator values into a vector then passing that and then passing a slice to `Config::build` + +Instead we pass ownership of the iterator returned from `env::args` to `Config::build` directly. + +Now we need to update the definition of `Config::build`. + +Here is how we update the signature of `Config::build`. + +Note this still wont compile because we need to update the function body. +```rust +impl Config { + pub fn build( + mut args: impl Iterator, + ) -> Result { + // --snip-- +``` +The std library documentation for the `env::args` function sohws that the type of the iterator it returns is `std::env::Args` and that type implements the `Iterator` trait and returns `String` values. + +Now we updated the `Config::build` signature so the paramter `args` has a generic type wioth the trait bounds `impl Iterator` instead of `&[String]` + +This useage of the `impl Trait` syntax was discuess in the [Traits and Paramters](./Traits.md#traits-as-parameters). + +This means that `args` can be any type that implements the `Iterator` trait and returns `String` + +Because we take ownership of `args`, then we well will be mutating `args` by iterating over it. + +We add the `mut` keyword into the sepcification of the `argsg` paramter to ensure it is mutable + +### Using `Iterator` Trait Methods Instead of Indexing +Now we will fix the body of `Config::build` + +Due to how `args` implements the `Iterator` trait, we know we can call the `next` method on it + +Here is the update body +```rust +impl Config { + pub fn build( + mut args: impl Iterator, + ) -> Result { + args.next(); + + 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, + }) + } +} +``` + +Remember that the first value in the return value of `env::args` is the name of the program + +We want to ignore that, we first call `next` and do nothing with the return value to consume it from the iterator. + +Next we call `net` to get the value we want to put in the `query field` of `Config` + +If `next` returns a `Some`, we use a `match` to extract the value. + +If it returns `None`, it means not enough arguments were given and we return early with an `Err` value. + +We do the same thing for the `file_path` value. + +## Making Code Clearer with Iterator Adapters + +We can also take advantage of iterators in the `search` function in the I/O project. + +Here is the old version of the `search` function +```rust +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 +} +``` +To rewrite this code in a more concise way by using adapter methods. + +This helps us avoid having a mutable intermediate `results` vector. + +The functional programming style prefers to minimize the amount of mutable state to make code clearer. + +Removing the mutable state might enable a future enhancement to make searching happen in parallel, because we wouldn't have to manage concurrent access to the `results` vector + +Here is the new change +```rust +pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { + contents + .lines() + .filter(|line| line.contains(query)) + .collect() +} +``` +The purpose of the `search` function is to return all lines in `contents` that contain the `query`. + +Similar to the filter example form before, this code uses the `filter` adapter to keep only the lines that `line.contains(query)` returns `true`. + +We collect the matching lines into another vector with `collect`. + +This is much simpler. + +You can also make the change to use iterator methods in `search_case_insensitive` function as well. + +## Choosing Between Loops or Iterators +The next question is which sytle you should choose in your own code and why + +The original implementation of minigrep verses using iterators. + +Most Rust programmers prefer to use the iterator style. + +It is a bit tougher to get the hand of at first, once you get the feel for the various \ No newline at end of file diff --git a/Iterators and Closures.md b/Iterators and Closures.md index 0650f1c..c9d4208 100644 --- a/Iterators and Closures.md +++ b/Iterators and Closures.md @@ -12,7 +12,7 @@ It will discuss some features of Rust that are similar to features in many langu It will cover: - [*Closures*](./Closures.md) - a function-like construct you can store in a variable - [*Iterators*](./Iterators.md) - a way of processing a series of elements -- How to use colsure and iterators to improve the I/O project (minigrep) +- How to use colsure and iterators to [improve the I/O project (minigrep)](./Improving%20The%20IO%20Project.md) - The preformance of closures and iterators (Spoiler alert: they are faster than you might think!) We have already covered some other Rust freatures, such as pattern matchin and enums, that are also influenced by the functional style. diff --git a/minigrep/src/lib.rs b/minigrep/src/lib.rs index 2a49c6b..7ddf5ea 100644 --- a/minigrep/src/lib.rs +++ b/minigrep/src/lib.rs @@ -10,17 +10,43 @@ pub struct Config { } impl Config { - pub fn build(args: &[String]) -> Result { - if args.len() < 3 { - return Err("not enough arguments"); - } + // refactor 14 + // pub fn build(args: &[String]) -> Result { + // if args.len() < 3 { + // return Err("not enough arguments"); + // } - let query = args[1].clone(); - let file_path = args[2].clone(); + // 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 }) + // } + + pub fn build(mut args: impl Iterator,) -> Result { + // --snip-- for now + + // skip first value in iterator, value inside is name of program + args.next(); + + 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 }) + Ok(Config { + query, + file_path, + ignore_case, + }) } } @@ -49,17 +75,23 @@ pub fn run(config: Config) -> Result<(), Box> { } pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { - // original that only can fail - // vec![] - let mut results = Vec::new(); + // refacotr 15 + // // original that only can fail + // // vec![] + // let mut results = Vec::new(); - for line in contents.lines() { - if line.contains(query) { - // do something with line - results.push(line); - } - } - results + // for line in contents.lines() { + // if line.contains(query) { + // // do something with line + // results.push(line); + // } + // } + // results + + contents + .lines() + .filter(|line| line.contains(query)) + .collect() } pub fn search_case_insensitive<'a>( diff --git a/minigrep/src/main.rs b/minigrep/src/main.rs index 85a7cc2..0d9c867 100644 --- a/minigrep/src/main.rs +++ b/minigrep/src/main.rs @@ -6,7 +6,8 @@ use std::process; use minigrep::Config; fn main() { - let args: Vec = env::args().collect(); + // refactor 13 + // let args: Vec = env::args().collect(); // dbg!(args); @@ -22,11 +23,17 @@ fn main() { // refactor 3 // let config = Config::new(&args); - // recfactor 6 - let config = Config::build(&args).unwrap_or_else(|err| { - // refactor 14 - // println!("Problem parsing arguments: {err}"); - // process::exit(1); + // // recfactor 6 + // let config = Config::build(&args).unwrap_or_else(|err| { + // // refactor 14 + // // println!("Problem parsing arguments: {err}"); + // // process::exit(1); + // eprintln!("Problem parsing arguments: {err}"); + // process::exit(1); + // }); + + // refactor 13 + let config = Config::build(env::args()).unwrap_or_else(|err| { eprintln!("Problem parsing arguments: {err}"); process::exit(1); });