Chapter 18

Iterators

What do managers and Rust iterators have in common?
They can both look very productive while doing absolutely nothing.

Rust iterators are lazy: they don't do any work until you ask for a result. That's great, because it means you can chain together many operations without creating intermediate collections.

You can filter, transform, and combine values in a single pipeline. And once you get the hang of it, you'll find that iterators are often more readable than a for loop. That's because you don't have to manage indexes or check bounds yourself; it's all part of Rust's ergonomic iterator machinery, one of my favorite parts of the language.

Here's how iterators work in practice:

  1. Get an iterator with .iter(), .into_iter(), .iter_mut(), or directly from .chars(), .lines(), etc.
  2. Chain adapters like .map(...), .filter(...), .take(...). These are lazy.
  3. Finish with a consumer like .collect(), .sum(), .count(), .any(...), or a for loop.
let words = vec!["hello", "café", "rust"];

let lengths: Vec<usize> = words
    .iter()
    .map(|word| word.chars().count())
    .collect();
assert_eq!(lengths, vec![5, 4, 4]);

The three "iter" methods differ in what they yield:

Some adapters change the item type. Here, iter yields &&str, but map produces character counts of type usize. The compiler infers types through the chain. You can write the chain first, then add a type annotation on the binding if the compiler needs one.

.collect() can produce many different collections. Tell it which one with a type annotation: Vec<_>, HashMap<_, _>, String. The _ lets the compiler fill in the inner types.

Coming Back to Word Count

Remember the three little functions from the word count chapter's exercise break? Each one was a counter, a for loop, and a return. With iterators, the whole trio shrinks to:

fn word_count(text: &str)   -> usize { text.split_whitespace().count() }
fn char_count(text: &str)   -> usize { text.chars().count() }
fn longest_word(text: &str) -> usize {
    text.split_whitespace().map(|w| w.chars().count()).max().unwrap_or(0)
}

You no longer have to maintain the mut counters or keep track of the maximum yourself.

Summing with an Iterator

A short warmup: calculate the total sales with an iterator consumer. The later exercises will combine consumers with adapters that change or select items.

You could add the values with a for loop and an accumulator. Here, sum asks the iterator for each value and collapses the sequence into one total.

Useful from the Standard Library

  • <[T]>::iter produces an iterator of shared references over the slice.
  • Iterator::sum reduces a numeric iterator to a single total. The function's i32 return type supplies its output type when you return the result directly. In other contexts, you may need an annotation (let total: i32 = ...) or the turbofish (.sum::<i32>()).
Exercise 1 of 6
Open in Web Editor

Results

    Compiler / runtime output
    
                
    Stuck? Show a hint No spoilers, just a nudge

    An array isn't an iterator yet. Once you have an iterator, which consumer produces a number rather than a collection? The function's return type can supply the numeric type.

    Reveal the full solution Spoiler: the complete answer
    /// Calculates total revenue from sales data.
    ///
    /// The simplest iterator pattern: take a sequence, produce one number. You
    /// could write a `for` loop with a running total, but the standard library can
    /// collapse a numeric iterator down for you in one call. See:
    /// <https://doc.rust-lang.org/std/iter/trait.Iterator.html>
    fn calculate_total_revenue() -> i32 {
        let sales = [1200, 850, 2300, 950, 1800, 3200, 1100, 2800];
        sales.iter().sum()
    }
    
    #[test]
    fn test_calculate_total_revenue() {
        let total = calculate_total_revenue();
        assert_eq!(total, 14200); // Sum of all sales
    }
    

    Transforming with `map`

    Normalize every email address to lowercase, preserving the input order. Unlike summing, this keeps one output for each input. Write an iterator pipeline that returns the new vector.

    map is lazy: it just describes the transformation. Nothing runs until collect (or another consumer) asks for the results.

    Useful from the Standard Library

    • Vec::into_iter consumes the vec and yields owned items. The input vector is no longer needed after this call. to_lowercase only borrows each string, so ownership is not required for the conversion itself.
    • Iterator::map applies a closure to each item and produces a new iterator with the transformed items.
    • Iterator::collect turns the pipeline back into a collection. The return type (Vec<String>) tells collect which collection to produce.
    • str::to_lowercase returns a fresh String with characters converted to lowercase. Lowercasing is not the same as Unicode case folding for caseless comparison.
    Exercise 2 of 6
    Open in Web Editor

    Results

      Compiler / runtime output
      
                  
      Stuck? Show a hint No spoilers, just a nudge

      Each input should produce exactly one output. With into_iter, your closure receives an owned String. The transformation returns another String; you don't need a mutable reference to the original.

      Reveal the full solution Spoiler: the complete answer
      /// Normalizes email addresses to lowercase.
      ///
      /// Return one normalized address per input, in the same order, using an
      /// iterator pipeline. See:
      /// <https://doc.rust-lang.org/std/string/struct.String.html#method.to_lowercase>
      fn normalize_emails(emails: Vec<String>) -> Vec<String> {
          emails
              .into_iter()
              .map(|email| email.to_lowercase())
              .collect()
      }
      
      #[test]
      fn test_normalize_emails() {
          let emails = vec!["Alice@EXAMPLE.COM".to_string(), "BOB@test.ORG".to_string()];
          let normalized = normalize_emails(emails);
          assert_eq!(normalized, vec!["alice@example.com", "bob@test.org"]);
      }
      

      Keeping Elements with `filter`

      With map you transformed every item, while filter keeps some items and drops the rest. There is one borrowing detail to watch: usernames.into_iter() yields &str, and filter gives its closure a reference to each item, so the closure sees &&str.

      Method calls such as s.starts_with(...) automatically dereference these layers. The extra references can be hard to track at first. If the compiler reports a missing &, check what the iterator yields and what the closure receives. The iterators entry in the cheatsheet shows those reference layers side by side.

      Useful from the Standard Library

      • Iterator::filter keeps only items where the predicate returns true. The closure receives a reference to the item, regardless of whether the iterator yields owned values or borrows.
      • str::starts_with takes a char (or another &str) and answers yes/no. Method-call syntax auto-derefs through the extra reference.
      • collect() here picks Vec<&str> straight from the return type. You don't need a turbofish here.
      Exercise 3 of 6
      Open in Web Editor

      Results

        Compiler / runtime output
        
                    
        Stuck? Show a hint No spoilers, just a nudge

        The predicate decides whether to keep an item; it doesn't return the item itself. With an iterator of &str, that predicate receives &&str. String method calls auto-deref through the extra reference.

        Reveal the full solution Spoiler: the complete answer
        /// Returns all users whose usernames start with 'a'.
        ///
        /// Same idea, but instead of transforming each element you keep some and drop
        /// others. Watch out for one borrowing gotcha: the closure receives a reference
        /// to each element, not the element itself. See:
        /// <https://doc.rust-lang.org/std/primitive.str.html#method.starts_with>
        fn select_usernames_starting_with_a(usernames: Vec<&str>) -> Vec<&str> {
            usernames
                .into_iter()
                .filter(|username| username.starts_with('a'))
                .collect()
        }
        
        #[test]
        fn test_select_usernames_starting_with_a() {
            let users = vec!["alice", "admin", "bob", "anonymous", "charlie"];
            let active = select_usernames_starting_with_a(users);
            assert_eq!(active, vec!["alice", "admin", "anonymous"]);
        }
        

        Filter, Then Own the Result

        This time the input is a &[&str], a borrowed slice of borrowed strings, so the iterator yields &&str. You'll return owned Strings so the caller can keep the results independently of the input. That lets us focus on the iterator chain without adding lifetime annotations.

        Combine what you've used so far to return only paths ending in .rs, in their original order. An empty input or no matching paths should give an empty vector. This time, choose how the adapters fit together yourself.

        Useful from the Standard Library

        • Iterator::filter borrows each item for its predicate. When the iterator yields &&str, the predicate receives &&&str. String method calls auto-deref through these layers.
        • Iterator::map can change the item type by applying a closure to each item.
        • str::to_string converts a borrowed string slice into an owned String. Auto-deref reaches through the extra reference for you.
        • str::ends_with checks whether a string ends with a given suffix.
        Exercise 4 of 6
        Open in Web Editor

        Results

          Compiler / runtime output
          
                      
          Stuck? Show a hint No spoilers, just a nudge

          There are two separate jobs: deciding which paths belong in the result and making the result independent of the input. Filtering alone doesn't change the item type. Where in your pipeline do borrowed strings become owned ones?

          Reveal the full solution Spoiler: the complete answer
          /// Finds all files with ".rs" extension.
          ///
          /// Same idea as the previous one, but the input is a `&[&str]` (a borrowed
          /// slice of borrowed strings), so the iterator yields `&&str`. We sidestep that
          /// double-reference by returning owned `String`s; the lesson here is iterators,
          /// not lifetimes. To go from `&&str` to `String`, reach for [`str::to_string`].
          fn find_rust_files(files: &[&str]) -> Vec<String> {
              files
                  .iter()
                  .filter(|file| file.ends_with(".rs"))
                  .map(|file| file.to_string())
                  .collect()
          }
          
          #[test]
          fn test_find_rust_files() {
              let files = &[
                  "main.rs",
                  "README.md",
                  "lib.rs",
                  "package.json",
                  "config.rs",
              ];
              let rust_files = find_rust_files(files);
              assert_eq!(rust_files, vec!["main.rs", "lib.rs", "config.rs"]);
          }
          

          Revisit: Summing Parsed Numbers

          In the ? chapter, you parsed each token inside a for loop. Write sum_numbers again, this time with an iterator pipeline and sum, without a loop or ?. Keep the same behavior: whitespace separates integers, empty input returns Ok(0), and the first parse error ends the sum. Assume the running total fits in an i32.

          Iterator::sum also works on an iterator of Result values. Its output can be a Result containing either the total or the first error. The function's return type can tell it which output type to use.

          Compare the two versions after the tests pass. In the loop, ? returns from sum_numbers when parsing fails. Here, sum stops asking for items and returns an Err to its caller; it does not return from the surrounding function. Your function can inspect that result or return it directly. Adding ? after sum would propagate an error that sum has already found.

          Exercise 5 of 6
          Open in Web Editor

          Results

            Compiler / runtime output
            
                        
            Stuck? Show a hint No spoilers, just a nudge

            What type does parsing each token produce? Compare that with the items a fallible sum can consume. If you unwrap each parse result first, what happens to the error you were supposed to return?

            Reveal the full solution Spoiler: the complete answer
            /// Sums whitespace-separated integers with an iterator pipeline and fallible
            /// `sum`.
            fn sum_numbers(text: &str) -> Result<i32, std::num::ParseIntError> {
                text.split_whitespace()
                    .map(|token| token.parse::<i32>())
                    .sum()
            }
            
            #[test]
            fn test_sum_numbers() {
                assert_eq!(sum_numbers("5\n10\n15").unwrap(), 30);
                assert_eq!(sum_numbers("  1  2  3 ").unwrap(), 6);
                assert!(sum_numbers("5\nabc\n15").is_err()); // not a number
            }
            
            #[test]
            fn test_sum_numbers_empty_and_signed() {
                assert_eq!(sum_numbers(""), Ok(0));
                assert_eq!(sum_numbers(" \t\n"), Ok(0));
                assert_eq!(sum_numbers("-5 +2 3"), Ok(0));
            }
            
            #[test]
            fn test_sum_numbers_first_error() {
                let first_error = "abc".parse::<i32>().unwrap_err();
                assert_eq!(sum_numbers("5 abc 99999999999999999999"), Err(first_error));
            }
            

            Predict, Then Run: How Far Does It Go?

            The tests below already contain the pipelines. Before running them, replace each todo!() with your prediction; leave the pipelines unchanged.

            For the first test, predict how many times the closure runs when nothing consumes the iterator. For the second, predict the first result, the collected batch, the next result, and the inputs visited by map. Write down the order of the printed messages too. Then run the tests and compare the trace with your predictions. Locally, use cargo test --example 17_iterators _8_lazy_consumption:: -- --nocapture to see output from passing tests.

            Does asking for one filtered result visit just one input? After collecting the batch, does the next call start over or resume? Try changing take(1) to take(2), predict all the results again, and rerun.

            Exercise 6 of 6
            Open in Web Editor

            Results

              Compiler / runtime output
              
                          
              Stuck? Show a hint No spoilers, just a nudge
              1. Creating a map adapter doesn't call its closure. Something must ask for an item.
              2. filter may request several inputs before it can yield one output.
              3. by_ref borrows the existing iterator; consuming that borrow advances the original too.
              Reveal the full solution Spoiler: the complete answer
              #[test]
              fn experiment_without_consumption() {
                  let mut calls = 0;
                  {
                      let _pipeline = (1..=8).map(|number| {
                          calls += 1;
                          number * 10
                      });
                      println!("pipeline constructed");
                  }
                  println!("closure calls: {calls}");
                  let expected_calls: usize = 0;
                  assert_eq!(calls, expected_calls);
              }
              
              #[test]
              fn experiment_partial_consumption() {
                  let mut visited = Vec::new();
                  let (first, batch, next) = {
                      let mut pipeline = (1..=8)
                          .map(|number| {
                              visited.push(number);
                              println!("map visits {number}");
                              number * 10
                          })
                          .filter(|number| number % 20 == 0);
                      println!("pipeline constructed");
                      let first = pipeline.next();
                      println!("first: {first:?}");
                      let batch: Vec<_> = pipeline.by_ref().take(1).collect();
                      println!("batch: {batch:?}");
                      let next = pipeline.next();
                      println!("next: {next:?}");
                      (first, batch, next)
                  };
                  println!("visited: {visited:?}");
              
                  let expected_first: Option<i32> = Some(20);
                  let expected_batch: Vec<i32> = vec![40];
                  let expected_next: Option<i32> = Some(60);
                  let expected_visited: Vec<i32> = vec![1, 2, 3, 4, 5, 6];
                  assert_eq!(first, expected_first);
                  assert_eq!(batch, expected_batch);
                  assert_eq!(next, expected_next);
                  assert_eq!(visited, expected_visited);
              }
              

              Wrapping Up Iterators

              You summed a numeric array with sum, transformed every element with map, kept just the matching ones with filter, and combined filter with map to convert borrowed slices into owned strings.

              What We Learned

              • An iterator pipeline starts with .iter(), .iter_mut(), .into_iter(), or a method such as .chars() or .lines(). Lazy adapters describe what should happen to each item. A consumer finishes the pipeline by asking for results.

              • iter yields &T, and iter_mut yields &mut T. On an owned Vec<T>, into_iter consumes the vector and yields T. On borrowed collections, it yields references instead: &T for &Vec<T> and &mut T for &mut Vec<T>. Pick the one that matches what you intend to do with each item.

              • Adapters (map, filter, take, skip, ...) describe the pipeline but do nothing on their own. The actual work happens when a consumer (collect, sum, count, for loop) asks for results.

              • collect is generic over the target collection. The return type (or a turbofish like .collect::<Vec<_>>()) tells it what to build.

              • sum needs to know its output type. The function's return type can supply it; otherwise annotate the binding or use .sum::<i32>().

              • filter's closure always takes &T, so on a &str iterator you'll see &&str. Method calls auto-deref, so .starts_with(...) works through extra references; comparison operators sometimes need an explicit *.

              • The |x| ... syntax you've been seeing is a closure: an anonymous function passed as an argument.

              • Fallible sum stops at the first Err and returns it as a value. Unlike ?, it doesn't return from the surrounding function.

              • next and take can consume only part of a pipeline. With by_ref, you can resume the same iterator afterward; already-consumed items don't run again.

              Next chapter 19Word Frequencies

              Optional Chapters

              Extra practice to explore at your own pace. These chapters do not count toward course progress.

              ← Return to the Rust course curriculum