Enhancing Rust Range?

I think the problems with ranges are one of the reasons they aren't used more. They are under-utilized in APIs. To give a couple of examples of how they can be useful when powering through the frustrations:

  • clap's Arg::num_args. Previously we had distinct functions for number of args, minimum number of args, and maximum number of args. That is confusing in how they interact.
  • winnow's take_while. The inspiration for this had separate take_while0, take_while1, and take_while_m_n calls. If nothing else, this removes the question of whether m_n is inclusive or not.
  • toml_edit::TomlError uses it for error spans to simplify the process of documenting whether they are inclusive or not.
12 Likes

As a specific example, text_size - Rust, which is heavily used in rust-analyzer, probably wouldn't exist if Range<u32> was copy (to be fair, there are other reasons for that crate to exist, but ranges being non-copy is the single one which is a deal-breaker).

4 Likes

Ranges being non-copy is a common pain point in langdev-related activities. It causes every library to have it's own Span type, and requires you to type a bunch of boilerplate to convert them back and forth.

11 Likes

So, it seems to me a lot of people want Copy Range. But what about stuff like:

  • Reversible ranges (e.g. 10..1)?
  • Negative ranges (e.g. ..^1 or in context &[1,2,3][..^1] == [1, 2], &[0, 1, 2, 3, 4][^2..^0] == [4, 5] ?
2 Likes

I would call them "descending ranges" rather than reversible ones. The problem with descending ranges using the same syntax is that they become a footgun when used with dynamic bounds, where suddenly you're iterating backwards.

Maybe we should reserve various forms of extra range syntax in edition 2023, like ^.., <.., etc

4 Likes

10..1 cannot be changed to mean the sequence "10, 9, 8, ..., 2" because it's already a valid empty range:

assert_eq!((10..1).contains(&9), false);

Even if backwards compatibility was not a concern, I would still prefer the current meaning, because it's much simpler: a..b means the range of values x that are both a <= x and x < b, so you don't even have to compare the endpoints. Allowing reversed ranges would turn that logic into something like ((a <= x) & (x < b)) | ((b < x) & (x <= a)) (not just min(a,b) <= x < max(a,b) because for consistency a should probably be included and b excluded regardless of their mutual order).

For (statically) reversed iteration, Iterator::rev seems sufficient. For a (statically) reversed range, you can sort of do

use std::cmp::Reverse;
assert_eq!((Reverse(10)..Reverse(1)).contains(&Reverse(9)), true);

... although it's not very useful when you can't use that for iteration[1] or indexing[2]. Admittedly the repetition is noisy, but it's the same issue as when using BinaryHeap and need a min heap.

I would expect a range with a dynamic direction is rarely needed, and better served by something more general like (0..len).map(|n| start + step * n).

edit: Actually, connecting the dots from my two footnotes to the other question that was asked, Reverse<usize> and Range<Reverse<usize>> might be natural types for indexing something from the back:

let v = ['a','b','c','d','e','f'];
assert_eq!(v[Reverse(0)], 'f');
assert_eq!(v[Reverse(1)], 'e');
assert_eq!(v[Reverse(2)..], ['d','e','f']);
assert_eq!(v[Reverse(3)..Reverse(1)], ['c','d']);

This mostly avoids issues of the python-solution where using an index that is unexpectedly negative is hard to debug because it's not an error.


  1. Would it make sense to impl Step for Reverse<{integer}> as a decrement to enable iteration? Probably not when just reversing the iterator is an option. â†Šī¸Ž

  2. And what would reversed slicing even mean when you can't reverse the subslice in place â†Šī¸Ž

4 Likes

Well, here's a sketch of one way that could be solved:

Don't reverse the indexing, but instead have a newtype for the slice reversed. Then you can index into the reversed slice and get back another reversed slice. And you can always get the underlying forward slice out of it if needed to pass to something else.

7 Likes

Might be a bit late, but shouldn't you start an RFC about this? Or is there an RFC already? Otherwise I compiled the pre-RFC you can reuse/edit range copy RFC

1 Like

I think I agree with everything except the blanket impl - that seems suboptimal for people who want to write the manual impls too...

It feels like there would be a crazy amount of breakage without the blanket impl, but I would like to know what libraries should look like mid-transition and what the ultimate version looks like. Ultimately I'd expect libraries to be implementing Index<Span>, which possibly allows for a blanket impl back to the deprecated(?) type.

I'm also interested in a RFC, but I'm not sure if it has any chance to be accepted.

@scottmcm I saw you proposed this idea before in that Zulip stream but I didn't see much feedback. Did it eventually become a pre-RFC or something?

What I can gather is that while this is from 2020, Range not being Copy has been identified as problem for a long time (see here and here from 2015) and at the time the libs team was confident that Range must be an iterator (I can't find anyone suggesting it shouldn't) and therefore the Copy impl would error prone. But even in Rust 1.0, the for desugaring used IntoIterator, so range didn't need to be an iterator to be usable in for loops even back then.

#21809 (briefly) and #21846 discussed it being IntoIterator instead. Downsides: some ergonomics, inference. Floated alternative future-possibility idea: Add Copy later with a lint for footgunny uses.

#27186 was post-1.0 so it would have been a breaking change to remove the Iterator impl by that point.

Why not? Looking at Impl Copy for range RFC issue It seems like this enhancement has been on the books for some while now.

I guess it is a concrete type and not a trait, so it wouldn't matter too much.. I'll probably look at the Pre-RFC and try pushing it forward in a few days.

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