DEV Community

Cover image for 50 Ways to Handle Your Iterator in Rust
Mete Can Eris
Mete Can Eris

Posted on

50 Ways to Handle Your Iterator in Rust

cover image by Karen Rustad Tölva.

The answer is easy if you
Take it logically
I'd like to help you in your struggle
To be free
There must be fifty ways
To leave your lover

That was Paul Simon on lovers. This is me on Rust iterators (yeah, living the life). Maybe not 50, but there are a lot of ways to handle your iterator in Rust. And they are equally easy if you take them logically.

Iterator, IntoInterator, and FromIterator traits

There are three main traits that we should look into to grasp iterators in Rust.

The Iterator trait requires the implementation of a single method called next(), which, when called, returns an Option<Item>.

  pub trait Iterator {
      type Item;

      fn next(&mut self) -> Option<Self::Item>;
  }
Enter fullscreen mode Exit fullscreen mode

Calling next() will return Some(Item) as long as there are elements, and once they’ve all been exhausted, it will return None to indicate that iteration is finished.

In the code below, there is an implementation of the Iterator trait for a certain Counter type (the example is from the Introduction to Programming Using Rust book).

  struct Counter {
      max: i32,
      // `count` tracks the state of this iterator
      count: i32,
  }

  impl Counter {
      fn new(max: i32) -> Counter {
          Counter { count: -1, max: max }
      }
  }

  impl Iterator for Counter {
      type Item = i32;

      fn next(&mut self) -> Option<Self::Item> {
          self.count += 1;
          if self.count < self.max {
              Some(self.count)
          } else {
              None
          }
      }
  }

  for i in Counter::new(10) {
      println!("{}", i);
  }
Enter fullscreen mode Exit fullscreen mode

The IntoIterator trait requires the implementation of an into_iter() method. This method defines how any data type can be converted into an interator.

  pub trait IntoIterator {
      type Item;
      type IntoIter: Iterator;
      fn into_iter(self) -> Self::IntoIter;
  }
Enter fullscreen mode Exit fullscreen mode

Finally, the FromIterator trait, obviously, takes an iterator and returns a collection.

  pub trait FromIterator<A> {
      fn from_iter<T>(iter: T) -> Self
      where
          T: IntoIterator<Item = A>;
  }
Enter fullscreen mode Exit fullscreen mode

The synergy between these traits allows an important flexibility when getting a custom type to be iterable. A developer can, of course, implement the Iterator trait for the type and be done with it. But they can also cheat a bit, in a sense, by implementing the IntoIterator and FromIterator traits to produce an existing iterable type, such as a Vec and offload the iteration responsibility to this intermediate data type.

Let's look into how the above example for the Counter type could have been implemented this way by using Range from the standard library (again, the example is from the Introduction to Programming Using Rust book).

  struct Counter {
      max: i32,
      // No need to track the state, 
      // because this isn't an iterator.
  }

  impl Counter {
      fn new(max: i32) -> Counter {
          Counter { max: max }
      }
  }

  impl IntoIterator for Counter {
      type Item = i32;
      type IntoIter = std::ops::Range<Self::Item>;

      fn into_iter(self) -> Self::IntoIter {
          std::ops::Range{ start: 0, end: self.max }
      }
  }

  for i in Counter::new(10) {
      println!("{}", i);
  }
Enter fullscreen mode Exit fullscreen mode

into_iter, iter and iter_mut methods

We have already seen that the IntoIterator trait has only one required method, into_iter() which converts the thing implementing IntoIterator into, well, an iterator.

Additionally, collection types generally offer methods that provide iterators over references, called iter() and iter_mut(). All these do is borrow either an immutable or mutable reference to the collection and turn it into an iterator.

In other words, if you're confused when to use which, remember that the king is into_iter(), and iter() and iter_mut() are just wrapper methods to borrow and into_iter in one go.

  // assuming lovers is Vec<Lover>

  // Iter<Vec<Lover>>
  // because: impl<'a, T> IntoIterator for Vec<T>
  lovers.into_iter()  

  // Iter<&Vec<Lover>>
  // because: impl<'a, T> IntoIterator for &'a Vec<T>
  lovers.iter()       

  // Iter<&mut Vec<Lover>>
  // because: impl<'a, T> IntoIterator for &'a mut Vec<T>
  lovers.iter_mut()   
Enter fullscreen mode Exit fullscreen mode

for loops

Rust's for loop is just syntactic sugar for iterators.

So this:

  // assuming lovers is Vec<Lover>
  for lover in lovers {
      println!("{}", lover);
  }
Enter fullscreen mode Exit fullscreen mode

is actually this:

  // assuming lovers is Vec<Lover>
  {
      let struggle = match IntoIterator::into_iter(lovers) {
          mut iter => loop {
              let next;

              match iter.next() {
                  // that's what she said
                  Some(lover) => next = lover,

                  // said no one ever 
                  None => break,                 
              };

              let lover = next;

              let () = { println!("{}", lover); };
          },
      };

      struggle
  }
Enter fullscreen mode Exit fullscreen mode

As we have seen, into_iter() takes self, using a for loop to iterate over a collection consumes that collection. Yet, it's possible to loop with for without consuming. Remember, for a Vec, the IntoIterator trait is implemented multiple times.

  impl<T> IntoIterator for Vec<T>
  impl<'a, T> IntoIterator for &'a Vec<T>
  impl<'a, T> IntoIterator for &'a mut Vec<T>
Enter fullscreen mode Exit fullscreen mode

Basically, if we loop over a vector directly, we'll consume it and get owned vector items within the loop. But if we loop over an immutable or mutable (& and &mut) reference to a vector, we'll get an immutable or mutable reference to vector items within the loop.

  // assuming lovers is Vec<Lover>


  // consuming lovers, lover is an owned Lover
  // ^_^ (see what I did there)
  for lover in lovers {}          

  // lover is &Lover
  // (because you can't own a lover)
  for lover in &lovers {}         

  // lover is &mut Lover
  // (but people don't change)
  for lover in &mut lovers {}     

Enter fullscreen mode Exit fullscreen mode

Lazy Iterators

As a final note, in Rust, iterators are lazy, meaning they have no effect until you call methods that consume the iterator.

Methods for consuming an iterator may be defined on the Iterator trait and are called consuming adapters because they fulfill whatever their purpose is by consuming the iterator via calling next() on it. Methods such as reduce(), find(), sum(), or collect() are consuming adapters.

On the other hand, certain methods called iterator adapters may also be defined on the Iterator trait. These take an iterator and return another. These methods are called between the creation of an iterator and its consumption. The primary example is, of course, map(). As a result of laziness, map() will do nothing unless fed into a consuming adapter.

Top comments (0)