Chapter 16

Iterators

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

Rust iterators are the best thing since &[bread].

Iterators turn "loop over a collection and do something" into a pipeline. They're lazy: nothing actually runs until you ask for a result. The compiler usually fuses chained iterator calls into a single tight loop, so the abstraction is free at runtime.

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, so trust it: write the chain, then add a type annotation on the binding if needed.

.collect() is interesting: it 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 chapter 5'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)
}

The loops, the mut counters, the running-maximum bookkeeping — all gone. That's the payoff for spending a chapter on iterators: every "walk a collection and reduce it to one number, or to one new collection" problem gets shorter and harder to get wrong.

Summing with an iterator

Iterators were popularised by functional languages like Lisp (created by John McCarthy in 1958), and today they're a core building block in most modern languages. Rust's iterators are lazy: they don't do any work until you ask for a result.

The simplest pattern is to take a sequence and collapse it down to a single value. You could write a for loop with a running total, but the standard library can do this for you in one call.

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. It's generic over the output type, so the compiler needs a hint: either annotate the binding (let total: i32 = ...) or use the turbofish (.sum::<i32>()).
  • Iterator::product is the multiplicative cousin if you ever need a running product.
Exercise 1 of 4
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(). And you'll need a type annotation (let total: i32 = …, or .sum::<i32>()) because sum is generic.

    Transforming with `map`

    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().

    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. That's what lets the closure call .to_lowercase() on a String directly.
    • 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 the case folded.
    Exercise 2 of 4
    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.
      3. emails.into_iter().map(|e| e.to_lowercase()).collect()
        

      Keeping elements with `filter`

      Same idea as map, but instead of transforming each element you keep some and drop others. Watch out for one borrowing gotcha: .iter() yields &T, but filter gives its closure another reference on top, so the closure sees &&T.

      That's why you'll often see **c == ... or s.starts_with(...) (which auto-derefs) instead of plain c == .... Don't be alarmed when the compiler complains about a missing &, see the cheatsheet entry on iterators.

      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. No turbofish needed.
      Exercise 3 of 4
      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') Just Works.
        3. usernames.into_iter().filter(|s| s.starts_with('a')).collect()
          

        Filter, then own the result

        Same as the previous step, 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 Strings; the lesson here is iterators, not lifetimes.

        To go from &&str to String, reach for [str::to_string]. Chain it after your filter with a map, then collect into a Vec.

        Useful from the standard library

        • Iterator::filter again. Same closure structure; auto-deref still saves you for .ends_with(".rs").
        • Iterator::map is what converts the surviving &&strs into owned Strings.
        • str::to_string is the easy &str -> String call. Auto-deref reaches through the extra reference for you.
        • str::ends_with is the suffix check used by the predicate.
        Exercise 4 of 4
        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. Auto-deref still saves you 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.
          3. files
                .iter()
                .filter(|name| name.ends_with(".rs"))
                .map(|name| name.to_string())
                .collect()
            

          Wrapping up iterators

          You collapsed a numeric vector 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 is a pipeline: get one with .iter() / .iter_mut() / .into_iter() (or directly from things like .chars() and .lines()), chain lazy adapters, then finish with a consumer.
          • 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 and product need to know their output type. Annotate the binding or use .sum::<i32>() to keep the compiler happy.
          • filter's closure always takes &T, so on a &str iterator you'll see &&str. Method calls auto-deref, so .starts_with(...) Just Works; comparison operators sometimes need an explicit *.
          • The |x| ... syntax you've been seeing is a closure: an anonymous function passed as an argument. Iterators are where closures earn their keep.
          Next chapter 17Word Frequencies