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

  • chain Rust iterator adapters
  • use closures in iterator chains
  • collect a result to different containers

Prerequisites

For completing this exercise you need to have

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

Task

Calculate the sum of all odd numbers in the following string using an iterator chain:

//ignore everything that is not a number
1
2
3
4
five
6
7
∞
9
X
11

We have a template project for this exercise. You can replace the todo! item in the template with reader.lines() and continue "chaining" the iterators until you've calculated the desired result. Note that the template will only be able to find numbers.txt if you run cargo run from the exercise-templates/iterators directory. Running the binary from elsewhere in the workspace will give a File not found error.

If you need it, we have provided a complete solution for this exercise.

Knowledge

Iterators and iterator chains

Iterators are a way to chain function calls instead of writing elaborate for loops.

This lets us have a type safe way of composing control flow together by calling the right functions.

For example, to double every number given by a vector, you could write a for loop:

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 idea of running a procedure like 2 * v[idx] whilst indexing over the entire collection is called a map. Using iterator chains you could instead write something like:

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.

The first point is not in vain - the original snippet has a bug in the upper bound, since 0..=v.len() is inclusive! It should have been 0..v.len().

Finally, don't forget that iterators are lazy functions - they only carry out computation when a consuming adapter like .collect() is called, not when the .map() is added to the chain.

Iterator chains workflow advice

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.

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.

Turbofish syntax ::<>

Iterators sometimes struggle to figure out the types of all intermediate steps and need assistance.

let numbers: Vec<_> = ["1", "2", "3"]
    .iter()
    .map(|s| s.parse::<i32>().unwrap())
    // a turbofish in the `parse` call above
    // helps a compiler determine the type of `n` below
    .map(|n| n + 1)
    .collect();

This ::<SomeType> syntax is called the turbofish operator, and it disambiguates calling the same method but getting back different return types, like .parse::<i32>() and .parse::<f64>() (try it!)

Dealing with .unwrap()s in iterator chains

Intermediate steps in iterator chains often produce Result or Option.

You may be tempted to use unwrap / expect to get the inner values. However, there are usually better ways that don't require a potential panic.

Concretely, the following snippet:

    let numbers: Vec<_> = ["1", "2", "3"]
        .iter()
        .map(|s| s.parse::<i32>())
        .filter(|r| r.is_ok())
        .map(|r| r.expect("all `Result`s are Ok here"))
        .collect();

can be replaced with a judicious use of .filter_map():

    let numbers: Vec<_> = ["1", "2", "3"]
        .iter()
        .filter_map(|s| s.parse::<i32>().ok())
        .collect();

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.

We make a special emphasis on avoiding ".unwrap() now, refactor later" because later usually never comes.

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

Some iterator chains involve Item being a tuple. If so, it may be useful to destructure the tuple when writing the closure:

let x = [10, 20, 30];
let y = [1, 2, 3];
let z = x
  .iter()
  .zip(y.iter())
  .map(|(a, b)| a * b)
  .sum::<i32>();

Here, 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

In general, we also recommend using the Rust documentation to get unstuck. In particular, look for the examples in the Iterator 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

Copy or recreate the exercise-templates/iterators template to get started.

Step 2: Read the string data

Read the contents of iterators/numbers.txt line by line, and collect it all into one big String. Note that the lines() iterator gives us Result<String, std::io::Error> so let's only keep the lines that we were able to succesfully read from disk.

Solution
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("numbers.txt")?;
    let reader = BufReader::new(f);

    let file_lines = reader.lines()
        .filter_map(|line| line.ok())
        .collect::<String>();

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

    Ok(())
}

Step 3: Skip the non-numeric lines

Now let's check that each line is a a valid number, using .parse(). We'll be collecting everything into a Vec<i32>.

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.

Solution

If the use of filter_map here is unfamiliar, go back and reread the Dealing with .unwrap()s in iterator chains section.

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("numbers.txt")?;
    let reader = BufReader::new(f);

    let numeric_lines: Vec<i32> = reader.lines()
        .filter_map(|line| line.ok())
        .filter_map(|line| line.parse::<i32>().ok())
        .collect::<Vec<i32>>();
    println!("{:?}", numeric_lines);

    Ok(())
}

Step 4: Keep the odd numbers

Use a .filter() with an appropriate closure to keep only the odd numbers.

Solution
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("numbers.txt")?;
    let reader = BufReader::new(f);

    let odd_numbers = reader.lines()
        .filter_map(|line| line.ok())
        .filter_map(|line| line.parse::<i32>().ok())
        .filter(|num| num % 2 != 0)
        .collect::<Vec<i32>>();

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

    Ok(())
}

Step 5: Add the odd numbers

Take the odd numbers and sum() them.

Solution
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("numbers.txt")?;
    let reader = BufReader::new(f);

    let result = reader.lines()
        .filter_map(|line| line.ok())
        .filter_map(|line| line.parse::<i32>().ok())
        .filter(|num| num % 2 != 0)
        .sum::<i32>();

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

    Ok(())
}