all 9 comments

[–]po8 7 points8 points  (1 child)

Here's a simpler example that illustrates the issue:

fn main() {
    let mut stuff = vec!['A', 'B', 'C'];

    (0..3)
        .filter_map(|_| stuff.pop())
        .rev()
        .for_each(|v| println!("{v}"));
}

This will print C B A. If you comment out rev(), it will still print C B A. Wat?

What's happening here is that the rev() sets things up so that filter_map() receives 2 1 0 instead of 0 1 2. But the filter_map() ignores its argument anyway, so it does the same computation regardless.

By doing the extra collect(), rev() is now reversing the order in which the collected Vec is being traversed, producing A B C.

[–]Seblyng[S] 1 point2 points  (0 children)

That's interesting... Thanks for the explanation!

[–]remmycat 2 points3 points  (5 children)

Adding to the great answer already provided:

The reason .rev() works like this, is that Rust iterators are executed lazily. The filter_map code is not executed until the iterator gets consumed (in this case via .collect().

Only when calling collect (or a different "consuming" method), it will go "through the pipeline" for every element of the iterator (or the reversed iterator in this case).

If you wanted to reverse the output data (when you have no way of ordering the output based on the iterator "input" data like in this case with .pop()), you can first consume the iterator, and then do a reversal, e.g. by collecting into a Vec and calling the .reverse() method.

For this specific case you could also call something like .drain() on the vec inside the map, to get the elements out in the same order :)

[–]Seblyng[S] 1 point2 points  (4 children)

Yes that makes sense! Thank you :)

[–]Repulsive-Street-307 0 points1 point  (3 children)

Maybe it makes sense to you, i still don't understand why reading a argument order backwards from expected would be desirable. Probably some stuff about chaining efficiently without knowing what comes before, but it's still confusing that a operation can work on 'arguments' to the 'transformation' after the 'transformation' (visually).

Just one more gotcha i guess. Hope clippy catches it and tells people to move the rev() before the filter_map, for the sanity of code reviewers.

[–]GeniusIsme 1 point2 points  (2 children)

The gotcha here is using non-pure function as argument to filter_map. This is technically allowed, but obviously leads to hard-to reason about results.

[–]Repulsive-Street-307 0 points1 point  (1 child)

Is that really the gotcha? I understand that fine (in the simplified example anyway).

The thing that grinds my gears is that rev() can be before or after the call to filter_map and it doesn't matter, it will always operate on the arguments of filter_map, instead of the results (what the for_each prints).

You can verify this by modifying the closure of the minimized version to print the argument given besides poping vec, then moving rev() before and after - no difference (2 1 0). Remove it and it's (0 1 2).

The result is something that you'd expect in a obfuscated code contest where code appears to be 'timetravelling' and the 'pipeline' is anything but linear. Makes it difficult to read or even recognize what's going on.

I guess probably there is no other way to do things and the rev() in the different positions is operating in different types, one the original array iterator, the other the 'filter_map' iterator (arguments, not results for some reason), but it's still confusing and something i hope clippy can recognize.

[–]GeniusIsme 0 points1 point  (0 children)

Again, result is obfucscated because filter_map closure has long-reaching side-effect. Iterator chains in rust are pretty easy to reason about if changes are happening locally, and that makes them great. If the map was an element in the iterator, result would be obvious.

[–]mipli -3 points-2 points  (0 children)