diff --git a/exercise-book/src/iterators.md b/exercise-book/src/iterators.md new file mode 100644 index 00000000..89ebcb4a --- /dev/null +++ b/exercise-book/src/iterators.md @@ -0,0 +1,324 @@ +# Iterators + +In this exercise, you will learn to manipulate and chain iterators. Iterators are a functional way to write loops and control flow logic. + +## After completing this exercise you are able to + +- write a Rust iterator +- use closures in iterator chains +- collect a result to different containers +- turn off rust-analyzer inlay hints + +## Prerequisites + +For completing this exercise you need to have + +- knowledge of control flow +- how to write basic functions +- know basic Rust types + +## Task + +- Add the odd numbers in the following string using an iterator chain + +```text +//ignore everything that is not a number +1 +2 +3 +4 +five +6 +7 +∞ +9 +X +``` + +- Take the template in [exercise-templates/iterators](../../exercise-templates/iterators/) as a starting point. +- Replace the first `todo!` item with [reader.lines()]() and continue "chaining" the iterators until you've calculated the desired result. +- Run the code with `cargo run --bin iterators1` when inside the `exercise-templates` directory + +If you need it, we have provided a [complete solution](../../exercise-solutions/iterators/src/bin/iterators1.rs) for this exercise. + +## Knowledge + +### Iterators and iterator chains + +Iterators are a way to write for loops in a functional style. The main idea is to take away the error prone indexing and control flow by giving them a name that you and others can understand and compose safely. + +For example, to double every number given by a vector, you could write a for loop: + +```rust +let v = [10, 20, 30]; +let mut xs = [0, 0, 0]; + +for idx in 0..=v.len() { + xs[idx] = 2 * v[idx]; +} +``` + +In this case, the name we give to the procedure `2 * v[idx]` and juggling the index over the entire collection is a [map](https://doc.rust-lang.org/stable/std/iter/trait.Iterator.html#method.map). An idiomatic Rustacean would write something similar to the following (period indented) code. + +```rust +let v = [10, 20, 30]; +let xs: Vec<_> = v.iter() + .map(|elem| elem * 2) + .collect(); +``` + +No win for brevity, but it has several benefits: + +- Changing the underlying logic is more robust +- Less indexing operations means you will fight the borrow checker less in the long run +- You can parallelize your code with minimal changes using [rayon](https://crates.io/crates/rayon). + +The first point is not in vain - the original snippet has a bug in the upper bound, since `0..=v.len()` is inclusive! + +Think of iterators as lazy functions - they only carry out computation when called with a `.collect()` or similar, not the `.map()` itself. + +### Turbo fish syntax `::<>` + +Iterators sometimes struggle to figure out the types of all intermediate steps and need assistance. + +We can write + +```rust +let xs = v.iter() + .map(|elem| elem * 2) + .collect::>(); +``` + +instead to avoid having a `xs: Vec<_> = ...`. This `::` syntax is called the [turbo fish operator](https://doc.rust-lang.org/book/appendix-02-operators.html?highlight=turbo%20fish#non-operator-symbols), and it disambiguates calling the same method with different output types, like `.collect::>()` and `.collect::>()` (try it!) + +### Dereferences + +Rust will often admonish you to add an extra dereference (`*`) by comparing the expected input and actual types, and you'll need to write something like `.map(|elem| *elem * 2)` to correct your code. A tell tale sign of this is that the expected types and the actual type differ by the number of `&`'s present. + +Remember you can select and hover over each expression and rust-analyzer will display its type if you want a more detailed look inside. + +## Destructuring in closures + +Not all iterator chains operate on a single iterable at a time. This may mean joining several iterators and processing them together by destructuring a tuple when declaring the closure: + +```rust +let x = [10, 20, 30]; +let y = [1, 2, 3]; +let z = x.iter().zip(y.iter()) + .map(|(a, b)| a * b) + .sum::(); +``` + +where the `.map(|(a, b)| a + b)` is iterating over `[(10, 1), (20, 2), (30, 3)]` and calling the left argument `a` and the right argument `b`, in each iteration. + +## Step-by-Step-Solution + +⚠️ NOTICE! ⚠️ + +When starting out with iterators, it's very easy to be "led astray" by doing what is locally useful as suggested by the compiler. + +Concretely, our first solution will feel like a slog because we'll deal with a lot of `Option` and `Result` wrapping and unwrapping that other languages wouldn't make explicit. + +A second more idiomatic solution will emerge in `Step 6` once we learn a few key idioms from the standard library. + +You, unfortunately, relive similar experiences when learning Rust without knowing the right tools from the standard library to handle errors elegantly. + +🧘 END OF NOTICE 🧘 + +We highly recommend that you consider turning off `inlay hints` in your `rust-analyzer` settings to `offUnlessPressed`, as they can get very noisy very quickly. You can do this by searching for `inlay hints` and choosing the right option in `Settings > Editor > Inlay Hints > Enabled`. + +In general, we also recommend using the Rust documentation to get unstuck. In particular, look for the examples in the [Iterator](https://doc.rust-lang.org/stable/std/iter/trait.Iterator.html) page of the standard library for this exercise. + +If you ever feel completely stuck or that you haven’t understood something, please hail the trainers quickly. + +### Step 1: New Project + +Create a new binary Cargo project, check the build and see if it runs. + +Alternatively, use the [exercise-templates/iterators](../../exercise-templates/iterators/) template to get started. +
+ Solution + +```shell +cargo new iterators +cd iterators +cargo run + +# if in exercise-book/exercise-templates/iterators +cargo run --bin iterators1 +``` + +
+ +### Step 2: Read the string data + +Read the string data from a file placed in `iterators/numbers.txt`. +Use the `reader.lines()` method to get rid of the newline characters. +Collect it into a string with `.collect::()` and print it to verify you're ingesting it correctly. It should have no newline characters since `lines()` trimmed them off. + +
+ Solution + +```rust +#![allow(unused_imports)] +use std::io::{BufRead, BufReader}; +use std::fs::File; +use std::error::Error; + +fn main() -> Result<(), Box> { + use crate::*; + let f = File::open("../exercise-templates/iterators/numbers.txt")?; + let reader = BufReader::new(f); + + let file_lines = reader.lines() + .map(|l| l.unwrap()) + .collect::(); + println!("{:?}", file_lines); + + Ok(()) +} +``` + +
+ +### Step 3: Filter for the numeric strings + +We'll collect into a `Vec`s with [.parse()](https://doc.rust-lang.org/stable/std/primitive.str.html#method.parse) to show this intermediate step. + +Note that you may or may not need type annotations on `.parse()` depending on if you add them on the binding or not - that is, `let numeric_lines: Vec = ...` will give Rust type information to deduce the iterator's type correctly. + +
+ Solution + +```rust +#![allow(unused_imports)] +use std::io::{BufRead, BufReader}; +use std::fs::File; +use std::error::Error; + +fn main() -> Result<(), Box> { + use crate::*; + let f = File::open("../exercise-templates/iterators/numbers.txt")?; + let reader = BufReader::new(f); + + let numeric_lines = reader.lines() + .map(|l| l.unwrap()) + .map(|s| s.parse::()) + .filter(|s| s.is_ok()) + .map(|l| l.unwrap().to_string()) + .collect::>(); + println!("{:?}", numeric_lines); + + Ok(()) +} +``` + +
+ +### Step 4: Keep the odd numbers + +Use a [.filter()](https://doc.rust-lang.org/stable/std/iter/trait.Iterator.html#method.filter) with an appropriate closure. + +
+ Solution + +```rust +#![allow(unused_imports)] +use std::io::{BufRead, BufReader}; +use std::fs::File; +use std::error::Error; + +fn main() -> Result<(), Box> { + use crate::*; + let f = File::open("../exercise-templates/iterators/numbers.txt")?; + let reader = BufReader::new(f); + + let odd_numbers = reader.lines() + .map(|l| l.unwrap()) + .map(|s| s.parse()) + .filter(|s| s.is_ok()) + .map(|l| l.unwrap()) + .filter(|num| num % 2 != 0) + .collect::>(); + + println!("{:?}", odd_numbers); + + Ok(()) +} +``` + +
+ +### Step 5: Add the odd numbers + +Take the odd numbers, `.collect()` into a vector, and add them using a [.fold()](https://doc.rust-lang.org/stable/std/iter/trait.Iterator.html#method.fold). + +You will probably reach for a `.sum::()`, but `.fold()`s are common enough in idiomatic Rust that we wanted to showcase one here. + +
+ Solution + +```rust +#![allow(unused_imports)] +use std::io::{BufRead, BufReader}; +use std::fs::File; +use std::error::Error; + +fn main() -> Result<(), Box> { + use crate::*; + let f = File::open("../exercise-templates/iterators/numbers.txt")?; + let reader = BufReader::new(f); + + let result = reader.lines() + .map(|l| l.unwrap()) + .map(|s| s.parse()) + .filter(|s| s.is_ok()) + .map(|l| l.unwrap()) + .filter(|num| num % 2 != 0) + .collect::>() + .iter() + .fold(0, |acc, elem| acc + elem); + // Also works + //.sum::(); + + println!("{:?}", result); + + Ok(()) +} +``` + +
+ +### Step 6: Idiomatic Rust + +That first solution can be a *slog*. + +Try writing a shorter solution using a [.filter_map()](https://doc.rust-lang.org/stable/std/iter/trait.Iterator.html#method.filter_map). + +
+ Solution + +```rust +#![allow(unused_imports)] +use std::io::{BufRead, BufReader}; +use std::fs::File; +use std::error::Error; + +fn main() -> Result<(), Box> { + use crate::*; + let f = File::open("../exercise-templates/iterators/numbers.txt")?; + let reader = BufReader::new(f); + + let result = reader.lines() + .map(|l| l.unwrap()) + .filter_map(|s| s.parse().ok()) + .filter(|num| num % 2 != 0) + .sum::(); + + println!("{:?}", result); + + Ok(()) +} +``` + +
\ No newline at end of file diff --git a/exercise-solutions/Cargo.toml b/exercise-solutions/Cargo.toml index d84b3a1a..c87ced68 100644 --- a/exercise-solutions/Cargo.toml +++ b/exercise-solutions/Cargo.toml @@ -12,4 +12,5 @@ members = [ "tcp-server-exercises", "async-chat", "kani-linked-list", + "iterators", ] diff --git a/exercise-solutions/iterators/Cargo.toml b/exercise-solutions/iterators/Cargo.toml new file mode 100644 index 00000000..2652a8a1 --- /dev/null +++ b/exercise-solutions/iterators/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "iterators" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/exercise-solutions/iterators/numbers.txt b/exercise-solutions/iterators/numbers.txt new file mode 100644 index 00000000..25f1bb7e --- /dev/null +++ b/exercise-solutions/iterators/numbers.txt @@ -0,0 +1,13 @@ +//ignore everything that is not a number +1 +2 +3 +4 +five +6 +7 +∞ +9 +X +10 +11 \ No newline at end of file diff --git a/exercise-solutions/iterators/src/bin/iterators1.rs b/exercise-solutions/iterators/src/bin/iterators1.rs new file mode 100644 index 00000000..f88af967 --- /dev/null +++ b/exercise-solutions/iterators/src/bin/iterators1.rs @@ -0,0 +1,35 @@ +#![allow(unused_imports)] +use std::io::{BufRead, BufReader}; +use std::fs::File; +use std::error::Error; + +fn main() -> Result<(), Box> { + use crate::*; + let f = File::open("../exercise-solutions/iterators/numbers.txt")?; + let reader = BufReader::new(f); + + // Write your iterator chain here + let sum_of_odd_numbers: i32 = reader.lines() + .map(|l| l.unwrap()) // peel off each line from the BufReader until you're done + .map(|s| s.parse()) // try to parse the line as a number, yields a `Result` + .filter(|s| s.is_ok()) // keep the lines that actually parsed (they're the `Ok` variant) + .map(|l| l.unwrap()) // unwrap the succesful parses, which yield numbers + .filter(|num| num % 2 != 0) // keep the odd numbers + .collect::>() // collect the numbers into a vector + .iter() // iterate over the vector + .fold(0, |acc, elem| acc + elem); // fold over the vector and add the elements, yields an i32 + + assert_eq!(sum_of_odd_numbers, 31); + + // Idiomatic solution + let second_reader = BufReader::new(File::open("../exercise-solutions/iterators/numbers.txt")?); + let nicer_sum: i32 = second_reader.lines() + .map(|l| l.unwrap()) + .filter_map(|s| s.parse().ok()) // map a .parse() and filter for the succesful parses + .filter(|num| num % 2 != 0) + .sum::(); + + assert_eq!(nicer_sum, 31); + + Ok(()) +} diff --git a/exercise-templates/Cargo.toml b/exercise-templates/Cargo.toml index c43ed95e..a20016a1 100644 --- a/exercise-templates/Cargo.toml +++ b/exercise-templates/Cargo.toml @@ -5,4 +5,5 @@ members = [ "rustlatin/*", "tcp-echo-server", "async-chat/*", + "iterators", ] diff --git a/exercise-templates/iterators/Cargo.toml b/exercise-templates/iterators/Cargo.toml new file mode 100644 index 00000000..f1133ded --- /dev/null +++ b/exercise-templates/iterators/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "iterators" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "iterators1" +path = "src/bin/iterators1.rs" + +[dependencies] diff --git a/exercise-templates/iterators/numbers.txt b/exercise-templates/iterators/numbers.txt new file mode 100644 index 00000000..25f1bb7e --- /dev/null +++ b/exercise-templates/iterators/numbers.txt @@ -0,0 +1,13 @@ +//ignore everything that is not a number +1 +2 +3 +4 +five +6 +7 +∞ +9 +X +10 +11 \ No newline at end of file diff --git a/exercise-templates/iterators/src/bin/iterators1.rs b/exercise-templates/iterators/src/bin/iterators1.rs new file mode 100644 index 00000000..07b7b7ac --- /dev/null +++ b/exercise-templates/iterators/src/bin/iterators1.rs @@ -0,0 +1,17 @@ +#![allow(unused_imports)] +use std::io::BufReader; +use std::fs::File; +use std::error::Error; + +fn main() -> Result<(), Box> { + use crate::*; + let f = File::open("../exercise-templates/iterators/numbers.txt")?; + let reader = BufReader::new(f); + + // Write your iterator chain here + let sum_of_odd_numbers: i32 = todo!("use reader.lines() and Iterator methods"); + + assert_eq!(sum_of_odd_numbers, 31); + Ok(()) +} +