.rev() method on RangeTo?

RangeTo and RangeToInclusive don't implements Iterator, so this fails to compile:

for i in (..=10) { }

But it would be nice if we could write as:

for i in (..=10).rev() {
    print!("{} ", i);
    if i == 5 {
        break;
    }
}

The expected result is 10 9 8 7 6 5.

There seem to be two ways to make it possible:

  • just add .rev() method to RangeTo and RangeToInclusive (where Idx: Step).
  • separate DoubleEndedIterator into two traits, Iterator and ReverseIterator, as pointed out in this issue.

I wonder:

  • Are there any other ways?
  • Would the latter one be a breaking change?
1 Like

For comparison, here’s one solution that currently works:

std::iter::repeat_with({
    let mut i = 10;
    move || (i, i -= 1).0
})

(here’s a test in the playground)

which is indeed quite lengthy. I’m curious if there’s better approaches that I’m missing.

fn countdown(n: i32) -> impl Iterator<Item = i32> {
    std::iter::successors(Some(n), |&n| Some(n - 1))
}

Playground.
successors documentation.

1 Like

or (0..).map(|i| n - i)

3 Likes

Note that this one over/underflows in a different part of the iteration (but behaves the same when wrapping).

1 Like

And I think (i32::MIN..=10).rev() also works in most cases. But it is still fascinating to me to realize (..=10).rev(), because it is so clear what it means.

1 Like

Note that this one won’t have the infinite size_hint and TrustedLen impl that repeat_with or n.. does have.

2 Likes

I beg to differ. The documentation explicitly states that the problem is it's got no beginning. So how would .rev() decide when to stop? If you want (0..=10).rev() or i32::MIN..=10).rev() then you should write that. It is clear enough.

3 Likes

It doesn’t stop, that’s the whole point. Just like 42.. doesn’t stop.

The iterator is infinite; naturally it can eventually overflow because the integer range is finite, resulting in wrap-around on release mode, or panic in debug mode. But it saves the bounds check, which is why it’s useful, I guess.

5 Likes

Yes, because you can't implement supertrait methods in an subtrait impl block.

Hmm, with negative impl coherence you could even have rev on RangeTo<T> return Rev<RangeTo<T>>, and then implement Iterator for Rev<RangeTo<T>>...

No, I don't think that's correct. 42.. already doesn't stop by itself, but ..42 doesn't begin anywhere. However, it would have to have a definite first element in order to be an iterator in the first place, so the stdlib is perfectly correct in observing this – perhaps counter-intuitive, but technically necessary – asymmetry.

This seems like a fixable problem. I'm currently (slowly) starting to work on some RFC at the moment that should make such changes possible in a non-breaking manner; good to have another (potential) example use-case here.

1 Like

We are talking about implementing Iterator on (..=10).rev(), not ..=10 itself.
And (..=10).rev() has the first element: 10.

Yes, I get that, but then .rev() would need to be a non-Iterator method.

So I proposed two ways: (1) adding RangeTo::rev() method apart from Iterator::rev(), or (2) splitting DoubleEndedIterator into Iterator and ReverseIterator.
In fact I don't have a concrete idea for (2) without breaking changes, though.