Skip to content

Commit

Permalink
fleshed out iterators1 exercise
Browse files Browse the repository at this point in the history
  • Loading branch information
miguelraz committed Jan 15, 2025
1 parent 2004410 commit f854578
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 100 deletions.
242 changes: 161 additions & 81 deletions exercise-book/src/iterators.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,17 @@ In this exercise, you will learn to manipulate and chain iterators. Iterators ar
- write a Rust iterator
- use closures in iterator chains
- collect a result to different containers
- iterate over slices and strings
- add and use the `itertools` library methods
- calculate a number of characteristics of the string "҈B҈҈E҈ ҈N҈҈O҈҈T҈ ҈A҈҈F҈҈R҈҈A҈҈I҈҈D҈"
- check if the following parentheses are balanced "(()()(()()(()(((())))()(()())()())))((())())"
- turn off rust-analyzer inlay hints

## Prerequisites

For completing this exercise you need to have

- knowledge of control flow
- how to write basic functions
- basic types
- know basic Rust types

## Task 1
## Task

- Add the odd numbers in the following string using an iterator chain

Expand All @@ -40,15 +37,17 @@ 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 iter1` when inside the `exercise-templates` directory
- 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/iter1.rs) for this exercise.
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. For example, to double every number given by a vector, you could write a for loop:
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];
Expand All @@ -59,7 +58,7 @@ for idx in 0..=v.len() {
}
```

In this case, the name we give to the logic of `2 * v[idx]` and juggling the index over the entire collection is a [map](). An idiomatic Rustacean would write something similar to the following (period indented) code.
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];
Expand All @@ -68,11 +67,11 @@ let xs: Vec<_> = v.iter()
.collect();
```

This doesn't look like much of a win in terms of brevity, but it comes with a couple of benefits:
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
- You can parallelize your code with minimal changes using [rayon]()
- 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!

Expand All @@ -90,17 +89,17 @@ let xs = v.iter()
.collect::<Vec<i32>>();
```

instead to avoid having a `xs: Vec<_> = ...` that may need back and forth editing as we change the iterator. This `::<SomeType>` syntax is called the [turbo fish](), and change the entirety of the iterator: `.collect::<HashSet<i32>>()`.
instead to avoid having a `xs: Vec<_> = ...`. This `::<SomeType>` 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::<HashSet<i32>>()` and `.collect::<Vec<i32>>()` (try it!)

### Dereferences

Rust will give you feedback on when you need to add an extra dereference (`*`) by telling you about 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.
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:
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];
Expand All @@ -110,135 +109,216 @@ let z = x.iter().zip(y.iter())
.sum::<i32>();
```

where the `.map(|(a, b)| a + b)` is taking iterating over `[(10, 1), (20, 2), (30, 3)]` and calling the left argument `a` and the right argument `b`, in each iteration.
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.

This code is basically equivalent to having written the named version of the closure and feeding it to `.map()`:
## Step-by-Step-Solution

```rust
fn multiplier(x: i32, y: i32) -> i32 {
x * y
}
⚠️ NOTICE! ⚠️

let x = [10, 20, 30];
let y = [1, 2, 3];
let z = x.iter().zip(y.iter())
.map(multiplier)
.sum::<i32>();
```
When starting out with iterators, it's very easy to be "led astray" by doing what is locally useful as suggested by the compiler.

## Step-by-Step-Solution
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.

In general, we also recommend to use the Rust documentation to figure out things you are missing to familearize yourself with it, and the methods in the [Iterator](https://doc.rust-lang.org/stable/std/iter/trait.Iterator.html) page of the standard library.
🧘 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.
<details>
<summary>Solution</summary>

```shell
cargo new fizzbuzz
cd fizzbuzz
cargo new iterators
cd iterators
cargo run

# if in exercise-book/exercise-templates/iterators
cargo run --bin iterators1
```

</details>

### Step 2: Counting from 1 to 100 in `fn main()`
### Step 2: Read the string data

Print the numbers from 1 to 100 (inclusive) to console. Use a `for` loop.
Running this code should print the numbers from 1 to 100.
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::<String>()` and print it to verify you're ingesting it correctly. It should have no newline characters since `lines()` trimmed them off.

<details>
<summary>Solution</summary>

```rust
fn main() {
for i in 1..=100 {
println!("{}", i);
}
#![allow(unused_imports)]
use std::io::{BufRead, BufReader};
use std::fs::File;
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
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::<String>();
println!("{:?}", file_lines);

Ok(())
}
```

</details>

### Step 3: The function `fn fizzbuzz`
### Step 3: Filter for the numeric strings

✅ Function Signature
We'll collect into a `Vec<String>`s with [.parse()](https://doc.rust-lang.org/stable/std/primitive.str.html#method.parse) to show this intermediate step.

Create the function with the name `fizzbuzz`. It takes an unsigned 32-bit integer as an argument and returns a `String` type.
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<i32> = ...` will give Rust type information to deduce the iterator's type correctly.

<details>
<summary>Solution</summary>

```rust
fn fizzbuzz(i: u32) -> String {
unimplemented!()
#![allow(unused_imports)]
use std::io::{BufRead, BufReader};
use std::fs::File;
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
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::<i32>())
.filter(|s| s.is_ok())
.map(|l| l.unwrap().to_string())
.collect::<Vec<String>>();
println!("{:?}", numeric_lines);

Ok(())
}
```

</details>

✅ Function Body
### Step 4: Keep the odd numbers

Use if statements with math operators to implement the following rules:
Use a [.filter()](https://doc.rust-lang.org/stable/std/iter/trait.Iterator.html#method.filter) with an appropriate closure.

- If `i` is divisible by `3`, return the String "Fizz"
- If `i` is divisible by `5`, return the String "Buzz"
- If `i` is divisible by both `3` and `5`, return the String "FizzBuzz"
- If neither of them is true return the number as a String
<details>
<summary>Solution</summary>

```rust
#![allow(unused_imports)]
use std::io::{BufRead, BufReader};
use std::fs::File;
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
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::<Vec<i32>>();

println!("{:?}", odd_numbers);

Ok(())
}
```

</details>

Running this code should still only print the numbers from 1 to 100.
### 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::<i32>()`, but `.fold()`s are common enough in idiomatic Rust that we wanted to showcase one here.

<details>
<summary>Solution</summary>

```rust
fn fizzbuzz(i: u32) -> String {
if i % 3 == 0 && i % 5 == 0 {
format!("FizzBuzz")
} else if i % 3 == 0 {
format!("Fizz")
} else if i % 5 == 0 {
format!("Buzz")
} else {
format!("{}", i)
}
#![allow(unused_imports)]
use std::io::{BufRead, BufReader};
use std::fs::File;
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
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::<Vec<i32>>()
.iter()
.fold(0, |acc, elem| acc + elem);
// Also works
//.sum::<i32>();

println!("{:?}", result);

Ok(())
}
```

</details>

### Step 4: Call the function
### Step 6: Idiomatic Rust

Add the function call to `fn fizzbuzz()` to the formatted string in the `println!()` statement.
That first solution can be a *slog*.

Running this code should print numbers, interlaced with `Fizz`, `Buzz` and `FizzBuzz` according to the rules mentioned above.
Try writing a shorter solution using a [.filter_map()](https://doc.rust-lang.org/stable/std/iter/trait.Iterator.html#method.filter_map).

<details>
<summary>Solution</summary>

```rust
# fn fizzbuzz(i: u32) -> String {
# if i % 3 == 0 && i % 5 == 0 {
# format!("FizzBuzz")
# } else if i % 3 == 0 {
# format!("Fizz")
# } else if i % 5 == 0 {
# format!("Buzz")
# } else {
# format!("{}", i)
# }
# }

fn main() {
for i in 1..=100 {
println!("{}", fizzbuzz(i));
}
#![allow(unused_imports)]
use std::io::{BufRead, BufReader};
use std::fs::File;
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
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::<i32>();

println!("{:?}", result);

Ok(())
}
```

</details>
</details>
1 change: 1 addition & 0 deletions exercise-solutions/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ members = [
"tcp-server-exercises",
"async-chat",
"kani-linked-list",
"iterators",
]
4 changes: 3 additions & 1 deletion exercise-solutions/iterators/numbers.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ five
7
9
X
X
10
11
Loading

0 comments on commit f854578

Please sign in to comment.