The need for decreasing range syntax (5..0)

Creating a range with arbitrary numbers seems unnecessarily difficult.

As a beginner, your intuition is to simply write

a..b

But as a and b are arbitrary, it is possible that a>b; which create a bug.

So you create a function to fix this problem.

fn create_range(a: usize, b: usize) -> RangeInclusive<usize>  {
    if a <= b {
        (a..=b)
    } else {
       (b..=a).rev()
    }
}

But this won't compile, because the .rev() trait will return a Rev<RangeInclusive<_>> and not RangeInclusive<_>.

So, to my understanding, the solution is either

  1. Use the heap
fn create_range_boxed(a: usize, b: usize) -> Box<dyn Iterator<Item = usize>> {
    if a <= b {
        Box::new(a..=b)
    } else {
        Box::new((b..=a).rev())
    }
}
  1. Use a chain hack
fn create_range_chain(a: usize, b: usize) -> impl Iterator<Item = usize> {
    let mut part1 = None;
    let mut part2 = None;

    if a <= b {
        part1 = Some(a..=b)
    } else {
        part2 = Some((b..=a).rev())
    }

    part1
        .into_iter()
        .flatten()
        .chain(part2.into_iter().flatten())
}

From a memory perspective I don't understand why 0..5 is valid rust but not 5..0. From a new user, I feel like those solution makes the language less accessible.

Feel free to suggest any better alternative with the std lib. Not sure if it's the rights place for this discussion. Have a great day

The free functions in std::iter can be quite helpful for creating custom iterators. Here's a less hacky way to implement almost what you want: playground

Almost? My implementation gives half-open ranges. If you want inclusive ranges, the two endpoints aren't enough to represent all possible ranges, including an empty one. One way to get around that: playground

Though I don't know if it's the fundamental one, these hint at one reason the standard ranges only go one way; iterating would require another branch to check the direction to go, although that should optimize out in typical cases.

5..0 is valid — it's an range that contains nothing. Changing this would be breaking.

What is the actual need for this? The post title implies you're posting about a need, but the body doesn't demonstrate this at all.

11 Likes

Note that for i in a..b {...} should not be slower than for (int i = a; a < b; a++) {...} in C. Inserting runtime check to determine direction to the simple range would not be accepted.

8 Likes

At the very least, it seems that a lint for literal big..little would be a good idea, to note that it's an empty range, not a decreasing one.

21 Likes

Here's a simplification that exploits the fact that reverse ranges are empty:

fn create_range_chain(a: usize, b: usize) -> impl Iterator<Item = usize> {
    let (part1, part2) = if a <= b {
        (a..=b, 1..=0)
    } else {
        (1..=0, b..=a)
    };

    part1.chain(part2.rev())
}

An alternative outside std is to wrap it in Either, noting "Either<L, R> is an iterator if both L and R are iterators." You can also generate this for an arbitrary number of variants with auto_enums.

3 Likes

:paperclip: clippy::reversed_empty_ranges

9 Likes

Because in the standard library, Rust generally dislikes anything that starts to behave differently in off-by-one situations.

For example, .step_by(0) was originally defined to repeat the first element infinitely. But that's weird, because for every other step size, .step_by(n) returns no more elements -- fewer for everything but n == 1. So now .step_by(0) panics -- that is also handy for traits, because it means StepBy<Range<usize>> can be ExactSizeIterator, which it couldn't be if it were sometimes infinite.

Similarly, people often ask about why v[-1] doesn't work in Rust, when it would be possible to define it to return v[v.len() - 1] as is done in many languages. But again, it's the surprise thing. It's not clear that someone doing v[i - 1] when i == 0 actually wants the last item -- getting a clear panic is way easier to debug than weird behaviour from getting the wrong element. (Not to mention that it's faster to not have to check for it.)

Similar things apply to Ranges. I'm glad that (i+4)..j is never longer than i..j (well, at least in debug mode -- in release wrapping adds other problems, but that's a different issue). And related to the traits thing, there's no such thing as a reversed slice, so it would be weird for 5..0 to give 5 descending items but for v[5..0] to give either ascending items or nothing.

If anything, I've sometimes wished for stricter rules on ranges, so that i..j requires i <= j, and thus (i..j).len() is always j - i, instead of j.saturating_sub(i).

22 Likes

I can +1 this. I just ran into this working on the Advent of Code day 5 problem. This is came up for me when I was trying to traverse a "path" between two points. When handling diagonals, I was looking for something along the lines of:

let x_range = from.x..=to.x;
let y_range = from.y..=to.y;
for (x, y) in x_range.zip(y_range) {
  do stuff
}

I was able to get around this in the first part of the problem where only horizontal and vertical lines were considered with if statements. Ex:

let range = if from.x < to.x { from.x..=to.x } else { to.x..=from.x }
for x in range {
  let coordinate = (x, static_y);
  do stuff
}

but once I was working on diagonals, I ran into issues. I could not handle scenarios elegantly where for something like (0, 1) -> (1, 0). Reversing x without reversing the y broke (you're not going from (0, 0) -> (1, 1) not (0, 1) -> (1, 0)). As suggested, I did end up writing my own reverse iterator, but I agree it would be nice to add support for this, even given the comments above around slices. If nothing else, I'd agree with the comment around this being more strict. In this scenario, I was surprised this didn't work. I think a panic here would have been a better experience than an empty range.

1 Like

I doubt there would be any opposition to uplifting the lint from clippy to rustc and making it warn-by-default. It would only handle simple cases, but it's better than nothing.

4 Likes

You could always make a helper function or type, as long as you're okay with slightly (r(a,b) vs a..=b) more verbose syntax.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.