DEV Community

Cover image for [Rust Guide] 13.6. Methods That Consume and Produce Iterators
SomeB1oody
SomeB1oody

Posted on

[Rust Guide] 13.6. Methods That Consume and Produce Iterators

13.6.0 Before We Begin

During its design, Rust drew inspiration from many languages, and functional programming had a particularly strong influence on Rust. Functional programming often includes passing functions as values to parameters, returning them from other functions, assigning them to variables for later execution, and so on.

In this chapter, we will discuss some Rust features that are similar to what many languages call functional features:

  • Closures
  • Iterators (this article)
  • Improving the I/O Project with Closures and Iterators
  • Performance of Closures and Iterators

If you find this helpful, please like, bookmark, and follow. To keep learning along, follow this series.

13.6.1 Methods That Consume Iterators

In the standard library, the Iterator trait has some methods with default implementations. Some of them call next, which is why implementing the Iterator trait requires implementing next.

Methods that call next are called consuming adaptors because next consumes the iterator one element at a time until the iterator is exhausted.

For example, the sum method takes ownership of the iterator and repeatedly calls next to traverse the items, thereby consuming the iterator. As it iterates, it adds each item to a running total and returns the total when iteration is complete.

#[cfg(test)]
mod tests {
    #[test]
    fn iterator_sum() {
        let v1 = vec![1, 2, 3];
        let v1_iter = v1.iter();
        let total: i32 = v1_iter.sum();
        assert_eq!(total, 6);
    }
}
Enter fullscreen mode Exit fullscreen mode

13.6.2 Methods That Produce Other Iterators

The Iterator trait also defines other methods called iterator adaptors. They turn the current iterator into a different kind of iterator. You can also chain multiple iterator adaptors together to perform complex operations, and this style of code is quite readable.

Take map as an example. It takes a closure that is applied to each element of the iterator. It transforms each element of the current iterator into another element, and those new elements form a new iterator.

let v1: Vec<i32> = vec![1, 2, 3];
v1.iter().map(|x| x + 1);
Enter fullscreen mode Exit fullscreen mode

This code adds 1 to each element in the Vector.

There is nothing wrong with the code itself, but the compiler produces a warning:

$ cargo run
   Compiling iterators v0.1.0 (file:///projects/iterators)
warning: unused `Map` that must be used
 --> src/main.rs:4:5
  |
4 |     v1.iter().map(|x| x + 1);
  |     ^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: iterators are lazy and do nothing unless consumed
  = note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
  |
4 |     let _ = v1.iter().map(|x| x + 1);
  |     +++++++

warning: `iterators` (bin "iterators") generated 1 warning
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.47s
     Running `target/debug/iterators`
Enter fullscreen mode Exit fullscreen mode

Because Rust iterators are lazy, if you do not consume them (that is, if you do not call consuming adaptor methods), they do nothing. In other words, in this state it does not add 1 to the three elements in the Vector unless a consuming method is called:

let v1: Vec<i32> = vec![1, 2, 3];
let v2:Vec<_> = v1.iter().map(|x| x + 1).collect();
Enter fullscreen mode Exit fullscreen mode

Here collect is used as a consuming adaptor to collect the results into some kind of collection. Since collect can produce many collection types, we need to explicitly annotate v2 as a Vec<_>. The _ in Vec<_> tells the compiler to infer the element type.

Top comments (0)