almost done with ch13.3

This commit is contained in:
darkicewolf50 2025-02-24 15:35:01 -07:00
parent ef22125714
commit ba2884744b
4 changed files with 267 additions and 24 deletions

204
Improving The IO Project.md Normal file
View File

@ -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<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,
})
}
}
```
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<String> = 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<Item = String>,
) -> Result<Config, &'static str> {
// --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<Item = String>` 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<Item = String>,
) -> Result<Config, &'static str> {
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

View File

@ -12,7 +12,7 @@ It will discuss some features of Rust that are similar to features in many langu
It will cover: It will cover:
- [*Closures*](./Closures.md) - a function-like construct you can store in a variable - [*Closures*](./Closures.md) - a function-like construct you can store in a variable
- [*Iterators*](./Iterators.md) - a way of processing a series of elements - [*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!) - 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. We have already covered some other Rust freatures, such as pattern matchin and enums, that are also influenced by the functional style.

View File

@ -10,17 +10,43 @@ pub struct Config {
} }
impl Config { impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> { // refactor 14
if args.len() < 3 { // pub fn build(args: &[String]) -> Result<Config, &'static str> {
return Err("not enough arguments"); // if args.len() < 3 {
} // return Err("not enough arguments");
// }
let query = args[1].clone(); // let query = args[1].clone();
let file_path = args[2].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<Item = String>,) -> Result<Config, &'static str> {
// --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(); 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<dyn Error>> {
} }
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
// original that only can fail // refacotr 15
// vec![] // // original that only can fail
let mut results = Vec::new(); // // vec![]
// let mut results = Vec::new();
for line in contents.lines() { // for line in contents.lines() {
if line.contains(query) { // if line.contains(query) {
// do something with line // // do something with line
results.push(line); // results.push(line);
} // }
} // }
results // results
contents
.lines()
.filter(|line| line.contains(query))
.collect()
} }
pub fn search_case_insensitive<'a>( pub fn search_case_insensitive<'a>(

View File

@ -6,7 +6,8 @@ use std::process;
use minigrep::Config; use minigrep::Config;
fn main() { fn main() {
let args: Vec<String> = env::args().collect(); // refactor 13
// let args: Vec<String> = env::args().collect();
// dbg!(args); // dbg!(args);
@ -22,11 +23,17 @@ fn main() {
// refactor 3 // refactor 3
// let config = Config::new(&args); // let config = Config::new(&args);
// recfactor 6 // // recfactor 6
let config = Config::build(&args).unwrap_or_else(|err| { // let config = Config::build(&args).unwrap_or_else(|err| {
// refactor 14 // // refactor 14
// println!("Problem parsing arguments: {err}"); // // println!("Problem parsing arguments: {err}");
// process::exit(1); // // 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}"); eprintln!("Problem parsing arguments: {err}");
process::exit(1); process::exit(1);
}); });