More fun with iterators

I am currently reading a longer chapter about iterators.

My feeling was, that map() and cloned() produces a value, so I assumed that the two commented out lines in the example below would be correct. But they do not compile:

    let numbers = [1, 2, 3, 4, 5];
    let processed: Vec<i32> = numbers
        .iter()
        .map(|x| *x * 2) // now the item is a value?
        .filter(|&x| x > 5) // this works
        //.filter(|x| x > 5) // but why does this fail
        .collect();
    println!("{:?}", processed);

    let numbers = [1, 2, 3, 4, 5];
    let processed: Vec<i32> = numbers
        .iter()
        .cloned() // now the item is a value?
        .filter(|x| *x > 5) // this works
        //.filter(|x| x > 5)  // but why does this fail
        .collect();
    println!("{:?}", processed);

One more question: The API docs say that we should use cloned() as late as possible when chaining iterator adaptors for best performance. I would also assume, that we should use adaptors like filter() early to reduce the number of items, before doing other operations like map()?

filter always gives a reference to the item into the closure, even when that item is owned. Otherwise, the closure would consume the item, and it couldn't be returned from the iterator.

6 Likes

I guess you meant a non-reference? References are values too. This is more a question of types I think.


.filter passes a reference to the Item type. Note that iterator adapters are very general and the Item may not be Copy (or Clone). Compare and contrast filter_map.

Beyond that is the papercut that Ord isn't fully general in terms of comparing T to &T.


That's sensible. (How effective any of these actually are is, as always, best answered by representative benchmarking.)

I usually put .copied pretty early. Instinct says it probably doesn't matter for performance. Ergonomics is a big motivator for why.

2 Likes

Part of the reason that .copied() exists is to make it clear that it's just a Copy, not a Clone, and thus it's more reasonable to put it early in the chain.

So absolutely do that with .copied(), especially for tiny elements like u8.

Having .cloned() later in the chain is about avoiding superfluous String clones, for example, where the clone actually does something worth avoiding.

3 Likes

Thanks for all your fast and detailed explanations -- I have to think about it, iterators are not always that easy in Rust.

1 Like

Sorry, one more question. The following code works when I try it directly in the Rust playground. I think it works, because the into_iter() method consumes the collection, taking ownership of its elements and removing a layer of reference?

fn main() {
    let a = [0, 1, 2];
    let mut iter = a.into_iter().filter(|&x| x > 1); // only one  &
    assert_eq!(iter.next(), Some(2));
    assert_eq!(iter.next(), None);
}

The strange thing is, that it does not compile when called from a mdbook page. Then I get this error message:

   Compiling playground v0.0.1 (/playground)
error[E0308]: mismatched types
 --> src/main.rs:4:50
  |
4 |     let mut iter = a.into_iter().filter(|&x| x > 1); // only one  &
  |                                                  ^ expected `&_`, found integer
  |
  = note: expected reference `&_`
                  found type `{integer}`
help: consider dereferencing the borrow
  |
4 |     let mut iter = a.into_iter().filter(|&x| *x > 1); // only one  &
  |                                              +

error[E0308]: mismatched types
 --> src/main.rs:5:29
  |
5 |     assert_eq!(iter.next(), Some(2));
  |                             ^^^^^^^ expected `Option<&{integer}>`, found `Option<{integer}>`
  |
  = note: expected enum `Option<&{integer}>`
             found enum `Option<{integer}>`
help: try using `.as_ref()` to convert `Option<{integer}>` to `Option<&{integer}>`
  |
5 |     assert_eq!(iter.next(), Some(2).as_ref());
  |                                    +++++++++

For more information about this error, try `rustc --explain E0308`.
error: could not compile `playground` (bin "playground") due to 2 previous errors

I think the reason is, that mdbook uses Rust edition = "2015" by default, which is not that obvious.

So my question is, why does it not compile with Rust edition = "2015"? Was that a bug in the 2015 edition, or what has changed?

Well, I guess this is a very difficult question, and an answer is not that important. I will switch for the book to 2021 edition, and all should be fine. But I think I should ask the mdbook authors to use 2021 edition by default, or at least display that information in case of compile errors. These "Compiling playground v0.0.1 (/playground)" in not that helpful, took me nearly an hour to find the reason for this strange issue.

Originally, arrays didn’t have their own IntoIterator implementation, so the syntax [a, b, …].into_iter() would invoke the IntoIterator implementation for slice references &[] instead (via Deref coercion). In order to not break old code that relied on this behavior, the new IntoIterator implementation is hidden from this syntax in editions prior to 2021.


The reason that edition 2015 is the default for mdbook is almost certainly the same reason it is for Cargo— backwards compatibility. Books that were written before the edition mechanism came into existence in 2015 (2018?) should continue to compile correctly without changes. Adding an edition setting to the new project template, on the other hand, should be relatively uncontroversial.

4 Likes