Most idiomatic way to enumerate in reverse?

I have a slice that I want to walk over in reverse and need to know the index position. I initially tried:

for (i, register) in registers.iter().rev().enumerate() { ... }

but I quickly discovered that while the slice is processed in reverse, the index still counts up from zero. I thought the rev carried through to chained calls, but that was incorrect. Fortunately, a test caught it.

I could just treat i as an offset from the end and subtract my way through, but that sort of thing often leads to off-by-one errors. So, I opted to find a way to have i be the index (implicitly offset from 0). Currently, I'm doing:

for i in (0..registers.len()).rev() {
     let register = &registers[i];
     ...
}

It's not the ugliest thing in the world, but it lacks some of the elegance of my first approach. Is there a more idiomatic way to enumerate a slice in reverse and get the index of the current element?

1 Like

Order of the calls matters. enumerate().rev() will count from the end.

9 Likes

Why would it? And more importantly: how could it? The enumerate function simply produces an iterator that starts counting from 0 up to N-1. It doesn't know in what order its underlying iterator is supposed to yield its elements, and it shouldn't need to know, either. It's not like it actively and magically computes indices from values or the addresses of the elements that the underlying iterator produces: it literally just maintains a counter and increments it. If it tried to guess the iterator order based on the elements themselves, it would only work with very special iterators, and it would be a lot slower too.

So, if you enumerate the iterator [1, 2, 3], you will get [(0, 1), (1, 2), (2, 3)]. And if you enumerate [3, 2, 1], you will get [(0, 3), (2, 1), (1, 2)]. There's no magic in that; enumerate doesn't go "mmmhm, these numbers look like they are in descending order, so I better reverse my own indices as well". That would be nonsensical, very surprising, and impossible in the general case.

Therefore, what you are looking for is reversing the whole iterator after enumerating. E.g. if you first enumerate [1, 2, 3], you get [(0, 1), (1, 2), (2, 3)], and then you reverse that, which will yield [(2, 3), (1, 2), (0, 1)].

1 Like

It would not and rightfully so, but it could by std::iter::Rev overriding std::iter::Iterator::enumerate().

How so? Iterator::enumerate() is defined to return Enumerate<Self>, and Enumerate<T> counts upwards from 0 for all types T.

1 Like

Right. Haven't thought about that.

NB : Enumerate (enumerate) only implements DoubleEndedIterator (rev) for ExactSizeIterator.

Example, this works :

(0..20).enumerate().rev()

and this doesn't :

(0..).take(20).enumerate().rev()
2 Likes

Why doesn't take() implements ExactSizeIterator for ExactSizeIterators?

This isn't a terribly helpful response and the condescending editorializing is unnecessary. I thought it would work that way because counting from one direction but returning elements from the other direction doesn't strike me as being all that useful. I'm sure use cases exist, though, so it's probably for the best that it works consistently. The state for knowing to reverse the elements is already being tracked, though, so enumerate could change its behavior based on that state.

Your point has been made. I already knew my initial expectation was wrong and I admitted as much in my original post, so I don't know what's to be gained by beating on that drum. I'll think twice about asking questions here again in the future. If I do venture back here, I'll refrain from providing new-ish user usability feedback. While my expectations may have been misguided, it's unlikely I'm the only person that will ever make the same mistake. I thought that might be helpful to share. At the very least, I thought it would help steer me to the correct solution. I was mistaken.

5 Likes

It's not take the issue, it's the open ended range.

3 Likes

That is much cleaner. It didn't occur to me that I could reverse the pair of values from the enumerate call. Thank you very much.

There's nothing "condescending editorializing" in my response. I was trying to direct your thinking towards understanding the mechanism with which iterators work.

Things do not necessarily work based on what's useful, because some useful things are impossible, for instance. As I explained above, .rev().enumerate() doesn't work in the way it works because it was explicitly deemed "consistent" that this particular combination of iterator adaptors should purposefully return elements and indices in the opposite direction. Rather, it works like that because that's the only possible (and natural) way it can work, due to the implementation and the return type of .enumerate().

It was not the point to point out that your expectation was wrong. Again, I was trying to make you understand, by asking the initial two questions, that the result is a necessity, not an accident (nor a deliberately confusing behavior that someone put in the library on purpose).

2 Likes

I'd urge you to go back and re-read your first response. You started off basically asking why in the world would I ever think it would or even could work the way I expected? Then you went on to invoke magic as the only possible solution to changing the count direction, while anthropomorphizing the enumerator to further poke fun of my line of thinking. Then you threw in phrases like "nonsensical" and "very surprising" just to let me know how silly my expectation of that chain of calls was. After two paragraphs of talking down to me, you offered an actual solution.

You could have said "I see why you'd think that, but here's where you went wrong." Instead you went with "why would it?" for committing the mortal sin of thinking a reverse iterator would continue to work in reverse. I'm glad you're well-versed in Rust's internals. At first glance, to me, if the iterator (ExactSizeIterator) has a len() method, switching the count direction wouldn't be impossible.

So, yes, your post was condescending (or maybe patronizing) and mostly unhelpful. If you don't think it is, then I don't know what to tell you. I generally try to be quite charitable in interpreting text communication, but your combined use of emphatic formatting and colorful vocabulary really makes it difficult to give you the benefit of the doubt. If this is the sort of response I can expect to receive on here (on my very first post, no less), I'm all set. There are other places where I can ask questions and be treated with a basic level of respect.

4 Likes

Closing this topic; further discussion of appropriate behavior can go to private messages or emailed to me or to rust-mods@rust-lang.org.

I would like to ask people to please be more... considerate, I guess, in responding to beginner questions especially.

It's certainly good to demonstrate the process one can use to figure out an answer, but don't act as though it should be obvious. Nothing about programming is obvious to everyone. Expecting newcomers to be able to arrive at correct answers on their own from the docs or from first principles is not reasonable, when they are busy absorbing a ton of complex information and may not even know where to find all the docs, or what the principles are. And aside from that, it doesn't make people feel good.

7 Likes