Introduce a way to construct Range from start + length

Currently the only way to construct such range is via start..(start + length)

Instead, we would be able to do it via Range::from_len(start, length)

Or, via a new custom syntax start..+length

  • This would help avoid off-by-one errors / wrongly defined ranges (ex: start..length)
  • Doesn't have a possibility to express an invalid (start > end) range
  • Reduce boilerplate (repeating start)
  • Is generally the more natural way to express a range

(Instead of len/length it can be count/width/offset)

9 Likes

Reminder that, at least if you're slicing, [start..][..count] works wonderfully.

And soon™ things like try { slice.get(start..)?.get(..count)? } will work too.

6 Likes

This works, but just for slicing (and looks quite wacky to me). Range is a structure by itself, which can be used outside of slicing too, and can be used to store, well, a range, instead of defining separate start and len/end fields.

4 Likes

If there was impl BitAnd<RangeTo> for RangeFrom with Output=Range that returned the intersection (semantically very close to and logic) then you could do from.. & ..to

Extended to the rest of the Range types, the intersection operator could be useful for treating Ranges as general interval types.

Then the question is whether BitOr should be overloaded to mean union, but a union of 2 continuous intervals is not necessarily a continuous interval (representable by Range) so maybe not because it doesn't seem obvious what to return there.

4 Likes

To be honest, I think this issue is quite less annoying than not having Python-like shortcut for [:-1]

1 Like

I would argue the exact python syntax (a[..-1]) is a footgun, it may looks ok with literals, but it's a source of bug when it's a computed value.

I won't oppose [..Rev(1)] though.

11 Likes

It's maybe less a footgun in Rust due to strong integer typing; you know when it's a usize vs a isize. On the other hand, you may want a isize but forget to cast and have usize overflow auto-wrapping, so you enter another category of error prone. Anyway, I agree with you.

It's a bit annoying that sometime, you need [..s.len() - 1] but you already have a mutable reference on s so you need to extract s.len() in a variable, but that's rare enough to make with it.

1 Like

You can do s.split_last().unwrap().1

I think that this would be a useful feature. My code has several examples of converting a start and length to Range to pass it elsewhere. (Many of them are start..start + 1 to specify a single element.)

However, the computation can have a numeric overflow, if start + length overflows. The appropriate behavior would need to be considered in the design of this function. Since data[start..][..length] reliably panics if the length is too large, it might be appropriate for Range::from_len(start, length) to panic too, even if overflow-checks are disabled.

This means that your point "Doesn't have a possibility to express an invalid (start > end) range" doesn’t quite hold: on overflow, it will have to deal with that invalid input.

2 Likes

Also, @appleGun22, since you’re new here, note that in order to add Range::from_len(), you don't necessarily have to convince the audience here on internals.rust-lang.org; you only need to convince the standard library team, which takes API Change Proposals. In order to write a successful ACP you would need to flesh out your proposal a bit more, particularly with real-world examples of code it would improve, and the feedback here may help you prepare.

6 Likes

What trait bounds would be on T here for this from_len method? Because if there's a syntax sugar for it, Range doesn't have any constraints.

fn range_fun() {
    let string_range = "a".."b";
    let tuple_range = (0,)..(1,);
}
1 Like

Maybe we could have something like:

impl<T: Step> RangeFrom<T> {
    fn with_len(self, count: usize) -> Range<T> {
        self.start.clone()..self.start.forward(count)
    }
}

e.g. (start..).with_len(count)

Step can allow checked/unchecked variants as well.

11 Likes
(start..)[..count]

:thinking:

(but then do we have to allow (start..)[4] i.e. start + 4 or is it just disallowed? Anyway spitballing)

(start..).nth(4).unwrap()

3 Likes

Old thread containing an equivalent proposal: New range operator `..+`

The proposed ..+ syntax avoids the ambiguity with unary + in Rust, since it doesn't have such, nor can an number literals start with a +. However it's still ambiguous with a.. + b.

Without saying whether such an operator is a good idea, it would be a straight-forward edition change to have ..+ become an operator and just insist that all existing ..+ be changed to .. +. (Especially since I doubt there's any non-contrived ..+ in the wild.)

4 Likes

I think this is impossible, since indexing operator can only return values in places that already exist, given the signature

fn index(&self, index: Idx) -> &Self::Output;

(note the connected lifetime of &Self::Output to &self from elision)

and (start..)[..count] would then desugar to *Index::index(&(start..), ..count)). But start.. of course does not happen to already contain the result of calculating start..start+count (for every possible value of count) inside of it :wink:

a.. + b doesn’t actually parse successfully, since + has higher precedence than .. - so the only place this sequence of tokens can currently come up is in macros that never end up parsing them as an expression at all.

3 Likes

Does it work if Range :: Copy (as in #![feature(new_range_api)]) or still no? :face_with_bags_under_eyes:

No. You cannot return a reference to a local variable.

This could be added motivation for IndexMove / IndexGet:

2 Likes