Chapter 18

Iterators

👋 Anyone can read and edit this exercise. Sign up to save your progress.

With iterators, you can work through a collection without managing an index or checking its bounds yourself. You build a pipeline of operations, but they're lazy: the work waits until you ask for a result. With optimizations enabled, the compiler can often turn chained iterator calls into a loop comparable to one you would write by hand.

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 names = vec!["alice", "ADMIN", "bob"];

let active: Vec<String> = names
    .iter()                              // &&str
    .filter(|n| n.starts_with('a'))      // keep some
    .map(|n| n.to_lowercase())           // transform
    .collect();                          // back to Vec<String>
// active == ["alice"]

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

Some adapters change the item type. After .map(|n| n.to_lowercase()), the items are Strings, not &&strs. 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 running total is a good first place to see what an iterator consumer does. Rust's iterators are lazy, so they don't do any work until you ask for a result.

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 sales.iter().sum() 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
    1. The whole function body is one chained call. Start with sales.iter().
    2. There is a single-call consumer that adds up a numeric iterator.
    3. sales.iter().sum(). When you return this expression directly, the function's i32 return type tells sum which type to produce.
    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`

    Now you need to transform every element instead of collapsing the sequence. Read this pipeline from left to right: take ownership of the vector's items, transform each one, then collect the results into a 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
      1. into_iter() (consume the input vec) → map(...)collect().
      2. The closure receives an owned String. Call .to_lowercase() on it.
      Reveal the full solution Spoiler: the complete answer
      /// Normalizes email addresses to lowercase.
      ///
      /// Now you need to transform every element instead of collapsing the sequence.
      /// The pattern is `vec.into_iter()` -> some combinator that applies a closure
      /// -> back to a `Vec` via `collect()`. 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
        1. into_iter()filter(...)collect().
        2. Gotcha: filter's closure takes a reference to each item. Since the iterator yields &str, the closure parameter is &&str. Method calls auto-deref, so |s| s.starts_with('a') works without an explicit dereference.
        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.

        str::to_string converts each surviving &&str into an owned String through auto-deref. Chain it after your filter with a map, then collect into a Vec.

        Useful from the Standard Library

        • Iterator::filter passes &&&str to the predicate here: a reference to the iterator's &&str item. Method-call auto-deref lets you call .ends_with(".rs") directly.
        • Iterator::map applies your conversion closure to each surviving &&str.
        • str::to_string converts a borrowed string slice into an owned String. Auto-deref reaches through the extra reference for you.
        • str::ends_with is the suffix check used by the predicate.
        Exercise 4 of 6
        Open in Web Editor

        Results

          Compiler / runtime output
          
                      
          Stuck? Show a hint No spoilers, just a nudge
          1. Same as the previous one, but the closure now sees &&&str. Method-call auto-deref also works for .ends_with(".rs").
          2. The function returns Vec<String>, not Vec<&str>. Add a .map(...) step that converts each &&str into an owned String.
          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
            1. What type does parsing each token produce? Keep those Result values as the iterator's items.
            2. map can apply parsing to each token. The return type tells sum to produce Result<i32, ParseIntError>.
            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, iter_mut yields &mut T, into_iter moves out of the collection and yields 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.