Skip to content

Commit 9854185

Browse files
add iterators exercise (#161)
Co-authored-by: Andrei Listochkin (Андрей Листочкин) <andrei.listochkin@ferrous-systems.com>
1 parent 3b344a1 commit 9854185

File tree

10 files changed

+435
-0
lines changed

10 files changed

+435
-0
lines changed

exercise-book/src/SUMMARY.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
- [Fizzbuzz with match](./fizzbuzz-match.md)
1111
- [Rust Latin](./rustlatin.md)
1212
- [URLs, match, result](./urls-match-result.md)
13+
- [Iterators](./iterators.md)
1314
- [SimpleDB](./simple-db.md)
1415
- [Knowledge](./simple-db-knowledge.md)
1516
- [Step-by-Step Solution](./simple-db-solution.md)

exercise-book/src/iterators.md

+355
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
# Iterators
2+
3+
In this exercise, you will learn to manipulate and chain iterators. Iterators are a functional way to write loops and control flow logic.
4+
5+
## After completing this exercise you are able to
6+
7+
- chain Rust iterator adapters
8+
- use closures in iterator chains
9+
- collect a result to different containers
10+
11+
## Prerequisites
12+
13+
For completing this exercise you need to have
14+
15+
- knowledge of control flow
16+
- how to write basic functions
17+
- know basic Rust types
18+
19+
## Task
20+
21+
22+
- Calculate the sum of all odd numbers in the following string using an iterator chain
23+
24+
```text
25+
//ignore everything that is not a number
26+
1
27+
2
28+
3
29+
4
30+
five
31+
6
32+
7
33+
34+
9
35+
X
36+
```
37+
38+
- Do `cargo new iterators`
39+
- Place the above multi-line string into `iterators/numbers.txt`.
40+
- Drop this snippet into your `src/main.rs`:
41+
42+
```rust [], ignore
43+
#![allow(unused_imports)]
44+
use std::io::BufReader;
45+
use std::fs::File;
46+
use std::error::Error;
47+
48+
fn main() -> Result<(), Box<dyn Error>> {
49+
use crate::*;
50+
let f = File::open("../exercise-templates/iterators/numbers.txt")?;
51+
let reader = BufReader::new(f);
52+
53+
// Write your iterator chain here
54+
let sum_of_odd_numbers: i32 = todo!("use reader.lines() and Iterator methods");
55+
56+
assert_eq!(sum_of_odd_numbers, 31);
57+
Ok(())
58+
}
59+
60+
```
61+
62+
- Replace the first `todo!` item with [reader.lines()](https://doc.rust-lang.org/stable/std/io/trait.BufRead.html#method.lines) and continue "chaining" the iterators until you've calculated the desired result.
63+
- Run the code with `cargo run --bin iterators1` when inside the `exercise-templates` directory if you want a starting template.
64+
65+
If you need it, we have provided a [complete solution](../../exercise-solutions/iterators/src/bin/iterators1.rs) for this exercise.
66+
67+
## Knowledge
68+
69+
### Iterators and iterator chains
70+
71+
Iterators are a way to chain function calls instead of writing elaborate for loops.
72+
73+
This lets us have a type safe way of composing control flow together by calling the right functions.
74+
75+
For example, to double every number given by a vector, you could write a for loop:
76+
77+
```rust [], ignore
78+
let v = [10, 20, 30];
79+
let mut xs = [0, 0, 0];
80+
81+
for idx in 0..=v.len() {
82+
xs[idx] = 2 * v[idx];
83+
}
84+
```
85+
86+
In this case, the idea of the procedure `2 * v[idx]` and indexing over the entire collection is called 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:
87+
88+
```rust [], ignore
89+
let v = [10, 20, 30];
90+
let xs: Vec<_> = v.iter()
91+
.map(|elem| elem * 2)
92+
.collect();
93+
```
94+
95+
No win for brevity, but it has several benefits:
96+
97+
- Changing the underlying logic is more robust
98+
- Less indexing operations means you will fight the borrow checker less in the long run
99+
- You can parallelize your code with minimal changes using [rayon](https://crates.io/crates/rayon).
100+
101+
The first point is not in vain - the original snippet has a bug in the upper bound, since `0..=v.len()` is inclusive!
102+
103+
Think of iterators as lazy functions - they only carry out computation when a *consuming adapter* like `.collect()` is called, not the `.map()` itself.
104+
105+
### Iterator chains workflow advice
106+
107+
Start every iterator call on a new line, so that you can see closure arguments and type hints for the iterator at the end of the line clearly.
108+
109+
When in doubt, write `.map(|x| x)` first to see what item types you get and decide on what iterator methods to use and what to do inside a closure based on that.
110+
111+
### Turbo fish syntax `::<>`
112+
113+
Iterators sometimes struggle to figure out the types of all intermediate steps and need assistance.
114+
115+
```rust [], ignore
116+
let numbers: Vec<_> = ["1", "2", "3"]
117+
.iter()
118+
.map(|s| s.parse::<i32>().unwrap())
119+
// a turbofish in the `parse` call above
120+
// helps a compiler determine the type of `n` below
121+
.map(|n| n + 1)
122+
.collect();
123+
```
124+
125+
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 `.parse::<i32>()` and `.parse::<f64>()` (try it!)
126+
127+
### Dealing with `.unwrap()`s in iterator chains
128+
129+
Intermediate steps in iterator chains often produce `Result` or `Option`.
130+
131+
You may be compelled to use `unwrap / expect` to get the inner values
132+
133+
However, there are usually better ways that don't require a potentially panicking method.
134+
135+
Concretely, the following snippet:
136+
137+
```rust [], ignore
138+
let numbers: Vec<_> = ["1", "2", "3"]
139+
.iter()
140+
.map(|s| s.parse::<i32>())
141+
.filter(|r| r.is_ok())
142+
.map(|r| r.expect("all `Result`s are Ok here"))
143+
.collect();
144+
```
145+
146+
can be replaced with a judicious use of [.filter_map()](https://doc.rust-lang.org/stable/std/iter/trait.Iterator.html#method.filter_map):
147+
148+
```rust [], ignore
149+
let numbers: Vec<_> = ["1", "2", "3"]
150+
.iter()
151+
.filter_map(|s| s.parse::<i32>().ok())
152+
.collect();
153+
```
154+
155+
You will relive similar experiences when learning Rust without knowing the right tools from the standard library that let you convert `Result` into what you actually need.
156+
157+
We make a special emphasis on avoiding "`.unwrap()` now, refactor later" because later usually never comes.
158+
159+
### Dereferences
160+
161+
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.
162+
163+
Remember you can select and hover over each expression and rust-analyzer will display its type if you want a more detailed look inside.
164+
165+
## Destructuring in closures
166+
167+
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:
168+
169+
```rust [], ignore
170+
let x = [10, 20, 30];
171+
let y = [1, 2, 3];
172+
let z = x.iter().zip(y.iter())
173+
.map(|(a, b)| a * b)
174+
.sum::<i32>();
175+
```
176+
177+
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.
178+
179+
## Step-by-Step-Solution
180+
181+
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.
182+
183+
If you ever feel completely stuck or that you haven’t understood something, please hail the trainers quickly.
184+
185+
### Step 1: New Project
186+
187+
Create a new binary Cargo project and run it.
188+
189+
Alternatively, use the [exercise-templates/iterators](../../exercise-templates/iterators/) template to get started.
190+
<details>
191+
<summary>Solution</summary>
192+
193+
```shell
194+
cargo new iterators
195+
cd iterators
196+
cargo run
197+
198+
# if in exercise-book/exercise-templates/iterators
199+
cargo run --bin iterators1
200+
```
201+
202+
Place the string
203+
204+
```text
205+
//ignore everything that is not a number
206+
1
207+
2
208+
3
209+
4
210+
five
211+
6
212+
7
213+
214+
9
215+
X
216+
```
217+
218+
and place it in `iterators/numbers.txt`.
219+
</details>
220+
221+
### Step 2: Read the string data
222+
223+
Read the string data from a file placed in `iterators/numbers.txt`.
224+
Use the `reader.lines()` method to get rid of the newline characters.
225+
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.
226+
227+
<details>
228+
<summary>Solution</summary>
229+
230+
We'll get rid of the `.unwrap()` in the next section.
231+
232+
```rust [], ignore
233+
#![allow(unused_imports)]
234+
use std::io::{BufRead, BufReader};
235+
use std::fs::File;
236+
use std::error::Error;
237+
238+
fn main() -> Result<(), Box<dyn Error>> {
239+
use crate::*;
240+
let f = File::open("../exercise-templates/iterators/numbers.txt")?;
241+
let reader = BufReader::new(f);
242+
243+
let file_lines = reader.lines()
244+
.filter_map(|line| line.ok())
245+
.collect::<String>();
246+
247+
println!("{:?}", file_lines);
248+
249+
Ok(())
250+
}
251+
```
252+
253+
</details>
254+
255+
### Step 3: Skip the non-numeric lines
256+
257+
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.
258+
259+
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.
260+
261+
<details>
262+
<summary>Solution</summary>
263+
264+
If the use of `filter_map` here is unfamiliar, go back and reread the ``Dealing with .unwrap()s in iterator chains`` section.
265+
266+
```rust [], ignore
267+
#![allow(unused_imports)]
268+
use std::io::{BufRead, BufReader};
269+
use std::fs::File;
270+
use std::error::Error;
271+
272+
fn main() -> Result<(), Box<dyn Error>> {
273+
use crate::*;
274+
let f = File::open("../exercise-templates/iterators/numbers.txt")?;
275+
let reader = BufReader::new(f);
276+
277+
let numeric_lines: Vec<i32> = reader.lines()
278+
.filter_map(|line| line.ok())
279+
.filter_map(|line| line.parse::<i32>().ok())
280+
.collect::<Vec<i32>>();
281+
println!("{:?}", numeric_lines);
282+
283+
Ok(())
284+
}
285+
```
286+
287+
</details>
288+
289+
### Step 4: Keep the odd numbers
290+
291+
Use a [.filter()](https://doc.rust-lang.org/stable/std/iter/trait.Iterator.html#method.filter) with an appropriate closure.
292+
293+
<details>
294+
<summary>Solution</summary>
295+
296+
```rust [], ignore
297+
#![allow(unused_imports)]
298+
use std::io::{BufRead, BufReader};
299+
use std::fs::File;
300+
use std::error::Error;
301+
302+
fn main() -> Result<(), Box<dyn Error>> {
303+
use crate::*;
304+
let f = File::open("../exercise-templates/iterators/numbers.txt")?;
305+
let reader = BufReader::new(f);
306+
307+
let odd_numbers = reader.lines()
308+
.filter_map(|line| line.ok())
309+
.filter_map(|line| line.parse::<i32>().ok())
310+
.filter(|num| num % 2 != 0)
311+
.collect::<Vec<i32>>();
312+
313+
println!("{:?}", odd_numbers);
314+
315+
Ok(())
316+
}
317+
```
318+
319+
</details>
320+
321+
### Step 5: Add the odd numbers
322+
323+
Take the odd numbers, and add them using a [.fold()](https://doc.rust-lang.org/stable/std/iter/trait.Iterator.html#method.fold).
324+
325+
You will probably reach for a `.sum::<i32>()`, but `.fold()`s are common enough in idiomatic Rust that we wanted to showcase one here.
326+
327+
<details>
328+
<summary>Solution</summary>
329+
330+
```rust [], ignore
331+
#![allow(unused_imports)]
332+
use std::io::{BufRead, BufReader};
333+
use std::fs::File;
334+
use std::error::Error;
335+
336+
fn main() -> Result<(), Box<dyn Error>> {
337+
use crate::*;
338+
let f = File::open("../exercise-templates/iterators/numbers.txt")?;
339+
let reader = BufReader::new(f);
340+
341+
let result = reader.lines()
342+
.filter_map(|line| line.ok())
343+
.filter_map(|line| line.parse::<i32>().ok())
344+
.filter(|num| num % 2 != 0)
345+
.fold(0, |acc, elem| acc + elem);
346+
// Also works
347+
//.sum::<i32>();
348+
349+
println!("{:?}", result);
350+
351+
Ok(())
352+
}
353+
```
354+
355+
</details>

exercise-solutions/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ members = [
1212
"tcp-server-exercises",
1313
"async-chat",
1414
"kani-linked-list",
15+
"iterators",
1516
]
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[package]
2+
name = "iterators"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//ignore everything that is not a number
2+
1
3+
2
4+
3
5+
4
6+
five
7+
6
8+
7
9+
10+
9
11+
X
12+
10
13+
11

0 commit comments

Comments
 (0)