Why is does Rust's range return fewer elements?

consider these
Rust:

fn main() {
    let count = vec!(1,2,3,4,5,6,7,8,9,10);
    let test = &count[0..4];
    println!("{:?}",test);
}

Returns [1, 2, 3, 4]

Powershell

$count=1..10
$test=$count[0..4]
$test

Returns 1 2 3 4 5

Rust seems to break the range convention I've seen in all other languages I've ever used, I'm certain there's a reason for this as it's one of the most well designed languages I've ever seen.

So I'm curious as to the reasoning behind this design choice. Does anyone know why it does this?

Rust's syntax for Powershell's equivalent (i.e. inclusive range) is 0..=4

Default range in Rust is exclusive, because that's has been most convenient. for i in 0..4 happens to match a common pattern for(int i = 0; i < 4; i++) used in C.

Various languages have various approaches to this, and there are several possible variations of the syntax. There's been RFC for the range syntax which has gone through MASSIVE bikeshedding with hundreds of comments. At this point the case is closed — Rust's range syntax is final.

2 Likes

What's the list of all other languages that you have used? Is Python among them? And Haskell?

Primarily Powershell, Javascript, Ruby, and Java. I've used Python but it's been a while and I never really liked it as I prefer {} syntax to encapsulate function bodies, does it share this approach?

Is it possible it's related to iterating over sections allowing the end of the range to be a breaking point, so I could for instance call this to split a vector, and end up with 2 perfectly divided slices without any overlap?

&count[..4]
&count[4..]

This is an excellent reason. Understanding the whys makes it easier for me to remember .

This sounds like what I was looking for, you don't happen to have a link for this do you? I searched on users.rust-lang.org and internals.rust-lang.org and didn't spot it.

https://github.com/frol/rust-rfcs/blob/master/text/1192-inclusive-ranges.md

https://github.com/rust-lang/rust/issues/28237

1 Like

C++ programmers feel at home with Rust's exclusive range.
See C++ STL iterator.

Half-open ranges are better than the alternatives, as has been known since (at least) 1982: E.W. Dijkstra Archive: Why numbering should start at zero (EWD 831)

You can also try out the various options with different divide-and-conquer algorithms. It's much nicer to split a..b into a..kk..b than the alternatives.

(If this were something like VB that indexes from 1 instead of 0, then you still don't want inclusive-on-both-sides ranges; we'd want half-closed ranges like (0, N] to represent 1, 2, ..., N, and APIs would be "insert after index" instead of before. But thankfully Rust indexes from zero.)

1 Like

And I figure it may have come over during the opening days of Rust, but for it to have persisted there must have been a stronger reason for it (as was shown, ty @kornel) [1] [2]

Also because there's a chance I may have kicked some people's toes, I wanted to clarify that I don't think any particular language has the perfect way everyone must use, and neither that any particular language is the devil. I view the differences in widely accepted languages as (usually) serving a purpose based on it's specialization or it's origins. But just because something is done a particular way, doesn't mean we can't (or even shouldn't) look at it from time to time to make sure it's still the best way to handle that role. And that goes both ways, just because powershell is using inclusive ranges doesn't mean they necessarily should be. Understanding the true underlying reasons for doing it a particular way (rather than just because that's how they do it) is how you tell if it's actually right for this purpose; This is what I was actually trying to get, the deeper underlying reasons why the two different approaches exist. If one was strictly always worse it probably wouldn't have survived this long.

3 Likes

I read this backwards initially.
You're saying if I want an inclusive range I should be using this?
Rust:

fn main() {
    let count = vec!(1,2,3,4,5,6,7,8,9,10);
    let test = &count[0..=4];
    println!("{:?}",test);
}

^This. It may seem unconventional, but once you've got used to it you won't want to go back.
Alas slices like [..-3] aren't supported out of the box (yet?) but I believe ndarray supports indexing like that.

1 Like

I think the mismatch is between English and Programming. Half-open is strictly better for programming, but humans who speak English are used to inclusive, so scripty-type things keep following English.

This is actually an interesting case where you see off-by-one errors outside of coding -- is "Tuesday to Thursday" two days or three?

Another advantage is that the length of a range a..b is simply b-a.

1 Like

Agreed - it's similar to 'should array indices start at 0 or 1'.
Non-inclusive upper bounds just work better in the same way that 0 just works better for array indicies.

1 Like

Having been a proponent earlier in my life of both 0 and 1 as default lower bounds, and of inclusive upper bounds – both largely motivated to maximize understanding of intent when reading code – I have concluded that specification via half-open intervals based at 0 works better for most code, so that the range 0..n where the upper bound is exclusive, i.e., [0..n), has the same number of elements as the range 1..n where the upper bound is inclusive, i.e., [1..n].

The one problem with the exclusive upper bound is specifying that bound when the inclusive upper bound is the end of the type range. For me that's the most compelling reason why an alternative range syntax specifying an inclusive upper bound is required.

1 Like