New range operator `..+`

I propose to add support for start ..+ length expression, which is equivalent to start .. start + length (e.g. 100..+5 is the same as 100..105).

This is especially useful for slicing when start expression is quite long.

Also, I do not think start ..+ length needs a special separate range type, i.e. it can be just of type std::ops::Range with end set to start + length.

Do you find this feature useful?
  • Yes
  • Maybe
  • No
0 voters
3 Likes

It's not very discoverable, but [start..][..length] is a fairly standard trick to reslice given a start and a length.

As with any other proposal adding new operators, this has to contend with macro_rules! lexing treating it as two separate tokens and not preserving jointness information. This would mean passing code through a no-op ($($t:tt)*) => ($($t)*) macro could change how it parses, which is at best a giant footgun.

The first proposal which adds a new operator will probably set a precedent for further proposals to follow, but until that happens, any proposal needs some plan to address this and explain how the proposal is so beneficial that it outweighs whatever edition differences it would require.

12 Likes

Note that this is technically a breaking change even outside macros, I think, because someone could override RangeFrom<Foo>: Add<Foo>, for x..+y.

This I'm not convinced by, because of overflow issues.

Especially the type questions are complicated -- can I use 3_u8 ..+ 100_usize? What about 3_usize ..+ 100_u8?

And a type in the library has a lower bar to add than new syntax.

3 Likes

No, because 3_u8 .. 3_u8 + 100_usize is illegal.

3_usize .. 3_usize + 100_u8 is illegal too.

Think of a ..+ b as just a syntactic sugar for a .. (a + b). (And + in (a + b) has exactly the same overflow issues as in a ..+ b.)

Does that include the panic in debug and wrapping otherwise?

Note that one good thing about the [i..][..n] approach that @CAD97 mentioned is that it doesn't need to worry about overflow.

2 Likes

But ..= is not treated as two separate tokens (.. and =), right?

In almost all programming languages where ambiguity exists, a token comprises the longest possible string that forms a legal token, when read from left to right. [And obviously, ..+ should be a separate legal token along with ..=.] So, I do not see any problems with lexing.

Moreover, I searched for ..+ in a large Rust code base and did not find anything, so this change will not break any existing code.

I was worried that [start..][..len] would generate worse code than [start .. start+len] due to having two separate places to panic on, but apparently both have two separate panic jumps. In that case I'll stick with [start..][..len], because it's reasonably concise and already an existing syntax.

5 Likes

Yes, of course. What else could it be?

No, you need to worry about overflow for [i..][..n] also (it just will be "range end index n out of range" instead of "attempt to add with overflow"). The thing is i + n - 1 must be a valid index.

But you need to handle that for the [i..(i + n)] form too -- after all, i + n might be out of range too.

When I say "overflow" I mean very specifically integer overflow, which is only an issue for the form that does the addition before a bounds check.

Yeah, you are right. But this is a very rare case (when i + n causes integer overflow while i + n - 1 is not).

Actually, I don't have enough experience in Rust, and this feature is probably not a good fit for this language. (I have proposed ..+ operator for some other programming languages also to get a response.)

Syntax sugar needs to clear a high bar of usefulness to justify its addition to the language. In this case the functionality is almost trivial, and imho would be better served by a free function range(start, step), or a method on Range. The specific case of array slicing already has a DRY solution, even if it's slightly obscure. This makes a new range operator even harder to justify.

1 Like

It seems like a ..+= b is open.

(I don't like the proposal overall)

I wonder if this is something clippy could/should teach.

8 Likes

I didn't see if anyone raised this point yet - what about a() ..+ b, would a() get evaluated twice, as in a() .. (a() + b)? Or does it work like let start = a(); start..(start + b), in which case only Copy types are allowed (.. and ..= permit any type)?

I do think the feature is useful, but I would think that to be maximally useful this syntax ought to result in a new type (not Range). Using a different type (RangeOffset, perhaps) dodges the problem in the previous paragraph as well as any overflow issues as discussed upthread.

But then you still have to implement Iterator for RangeOffset... something like this?

struct RangeOffset<A, B> {
    start: A,
    length: B,
}

impl<A, B> Iterator for RangeOffset<A, B>
where
    A: Step + Add<B, Output=A>,
{

... here I must unfortunately leave the rest to someone who got more sleep last night.

3 Likes

If the use case is iteration, you can also do:

for i in (start..).take(len) {
   ...
}

(even though ranges probably shouldn't be iterators, sometimes it's convenient that they are!)


Ranges could have an offset method [1], allowing one to write (0..len).offset(start), though it's a bit confusing to have len before start. But the method itself could be useful in other situations too.


  1. or dare I say, Add impl :wink: ↩ī¸Ž

Shouldn't it be possible to create the range let start = a(); start..(b + start) instead, since + should (I assume) be commutative for types used in a range? It would make it possible to use non-Copy type that way.

I think he's asking about the exact desugaring. let start = a(); start..(b + start) doesn't work if start: !Copy. let start = a(); &start..&(&start + b) doesn't work either since

the trait `SliceIndex<[{integer}]>` is not implemented for `std::ops::Range<&usize>`

(although maybe it could be)