`...` vs `..=` for inclusive ranges

Here’s one use case for a>.. syntax and for the accompanying std::ops::RangeFromExclusive type. This is something I actually encountered with, so it’s not a contrived example.

You have a type BoundedIndex which encapsulates an integer and guarantees that any code located outside the module of BoundedIndex that asks for the value of the encapsulated integer is going to get an answer that’s within a certain range, say [0, 9] in this case. This type is useful for safe indexing without bounds checking into fixed-size arrays like [T; 10]. If you try to add an integer to a BoundedIndex, the resulting index is checked to be within the valid range like so:

impl Add<usize> for BoundedIndex {
    type Output = BoundedIndex;
    
    fn add(self, value: usize) -> BoundedIndex {
        assert!(value < 10 - self.index);
        
        BoundedIndex { index: self.index + value }
    }
}

In the module of BoundedIndex you have also a type Iter which implements the Iterator trait. It’s something like:

#[derive(Copy)]
pub struct Iter {
    front: BoundedIndex,
    after: BoundedIndex,
}

To create an Iter, you call an associated trait method called slice on BoundedIndex:

pub trait Slice<R> {
    fn slice(range: R) -> Iter<Self>;
}

// Here's the implementation for the regular "from-inclusive" `RangeFrom`:

impl Slice<RangeFrom<BoundedIndex>> for BoundedIndex {
    fn slice(range: RangeFrom<BoundedIndex>) -> Iter<BoundedIndex> {
        Iter {
            front: BoundedIndex { index: range.start.index },
            after: BoundedIndex { index: 10 }, // The "end" is one off bounds
        }
    }
}

// And here's the implementation for the hypothetical `RangeFromExclusive`:

impl Slice<RangeFromExclusive<BoundedIndex>> for BoundedIndex {
    fn slice(range: RangeFromExclusive<BoundedIndex>) -> Iter<BoundedIndex> {
        Iter {
            // We know that front index is going to be at most one off bounds
            // (which is fine, that would just mean it's an empty slice).
            front: BoundedIndex { index: range.excluded_start.index + 1 },
            after: BoundedIndex { index: 10 },
        }
    }
}

Now, compare these two different ways to try to create the exactly same slice:

  1. Using RangeFrom: BoundedIndex::slice(x+1 ..)

  2. Using RangeFromExclusive: BoundedIndex::slice(x >..)

…where x is a BoundedIndex.

The first case, using RangeFrom, goes through the add method and therefore has to check that the resulting index is within the valid range of values. Also, notice that if x is at the maximum index value 9, then this will cause panic in the add method.

The second case, using RangeFromExclusive, is more optimized because it doesn’t have to go through the add method nor bounds check the value of the index within x. This way of creating the slice won’t ever panic.

1 Like

My :+1::+1: for @tommit notation, which can be used equally well for ranges and patterns.

Moreover it would be nice to be able to write:

if x in 2.4>..3.2 {...}

Please notice I opened a similar thread on the users forum before knowing of this one:

I would like to specify that actually IMO the Mesa notation is the best one, but if the parser cannot manage unbalanced brackets I support @tommit notation instead.

Good aspects of the Mesa notation is that it is visually very clear (at least to me), and that having mandatory brackets helps when the type needs to be specified.

For instance with @tommit notation we would write: 0u8..<100 2.4f64>..3.2

Using Mesa notation we would write: [0..100)u8 (2.4..3.2]f64 which IMO is visually clearer.

Edit: even better would be: [0:100)u8 (2.4:3.2]f64 or: [0:100[u8 ]2.4:3.2]f64 The : notation is used by MATLAB and other numerical computing languages.

I agree with @lucatrv that the Mesa notation is very clear. I also don’t see any fundamental reason why parsers should not be able to handle unbalanced brackets (i.e. things like [0..100) or (0..1]f64, or more generally (EXPR].) — that said, many syntax highlighters are likely to trip up on this without fixes.

… but, is this enough? For example, given the ways brackets are currently interpreted, why would (a..b) not be interpreted as a..b and [x..y] as an array? Matching the .. inside might help with this (like (1,) is interpreted as a tuple), but complicates the parser.

And: (a..b) is currently legal syntax (for a..b). [a..b] is also legal (an array of length 1 containing a range). This proposal would change the meanings.

Using a unique separator (maybe ::) instead of .. would help work around the above issues, but is still not nice (though as long as the un-bracketed a::b is invalid syntax, it is no worse than the current tuple syntax).

TLDR: supporting [a..b) or probably [a:b) would break too much, while [a::b) might be feasible.

Just FYI, I think the window for making this change has closed – and I was convinced by the many comments here that .. vs ... is just fine anyway.

5 Likes

Yeah, I’m aware the existing syntax is probably not going away now, and have nothing against it anyway. But a bit miffed that a..b seems to work only in for expressions and 1...5 only in match expressions (and only with constants).

That’s something we intend to fix.

1 Like

I’m in favor of the status quo. I agree with others here that it’s not hard to remember that “more dots is more numbers”. I also find ..= to be remarkably ugly.

That said, I would not be adverse to adopting Swift’s syntax, using a..<b for [a,b) and a...b for [a,b]. Yeah, it’s one extra symbol, and exclusive ranges are the most common, but I think that’s ok. When Swift first made this change (it was first released with a..b for exclusive ranges) I was annoyed because I thought a..b was perfectly fine. But now that I’m used to it, I actually find it works pretty well in practice, and it has the benefit of reminding the reader that it’s an exclusive range (since inclusive ranges are so rare, using a separate symbol for an inclusive range doesn’t help the reader that isn’t familiar with the syntax yet). For context, my day job is iOS programming and I’ve been writing Swift exclusively for that for a while, so I’ve had plenty of practice using this syntax. And I think it’s not unreasonable to say that using a..<b for exclusive range syntax is a helpful reminder to my coworkers who are still primarily writing Obj-C when they have to read Swift source.

1 Like

As an inexperienced newcomer, the swift notation is more intuitive to me. When looking at the documentation, it seemed like the ranges were inclusive:

"This code, for example, does not actually generate the numbers 1-100, and just creates a value that represents the sequence:

let nums = 1…100;"

But this should actually mean the numbers 1 to 99?

To me, …< for exclusive and … for inclusive would be clear and would make sense. It’s easier to distinguish than “…” and “…”.

1 Like

1..100 represents the numbers 1 to 100, not including the end point :smile:

The meaning of .. is unlikely to change -- Rust 1.0 releases in 12 days!

I think having just [a,b) and [a,b] ranges makes most sense, since having start not be inclusive seems like a weird edge case.

Now as for concrete syntax I’m fine with either Swift’s ..</ ... or some kind of backwards compatible syntax (e.g. ... /..~, I don’t have much preference.

For discrete ranges, that’s fine. However, continuous ranges really need exclusive ends to be fully expressive.

How often do continuous ranges appear in practice?

Strings and floats are continuous so both the rand crate and BTreeMap needs continuous ranges. BTreeMap works around this by providing a range function that takes two Bounds (where bound is an enum: Included(idx), Excluded(idx), Unbounded) and the rand crate just ignores left-exclusive ranges.

While continuous ranges are common, you’re right that left-exclusive ranges are not very common (but I wouldn’t go so far as to call them a weird edge case). While I’d still like them to be supported in the standard library, I don’t think they need their own syntax.

2 Likes

Ok, I have a question relative to this discussion. Suppose I have a series of measurement real values, and I need to classify them. For values between 0 (included) to 10 (included) output “class 1”, for values between 10 (excluded) to 20 (included) output “class 2”, for values above 20 output “oversize”, for values below 0 emit an error. How do I program this in Rust using a match expression? Thanks

Currently, you would write:

match run_experiment() {
    0.0...10.0 => Ok("class 1"), // 0, 10 inclusive
    10.0...20.0 => Ok("class 2"), // technically includes 10 but matching is in-order
    other if other > 0.0 => Ok("oversize"), // guard > 0
    _ => Err(some_error), // fallback
}

Wow… I guess you agree that is not very simple to read, IMO this would be much better:

match run_experiment() {
    20.0>.. => Ok("oversize"),
    10.0>.. => Ok("class 2"),
    0.0... => Ok("class 1"),
    _ => Err(some_error),
}
1 Like

Personally, I'd be tempted to an if expression for something like that.

let v = run_experiment();
let result = if 0.0 <= v && v <= 10.0 {
    Ok("class 1")
...

In this case IMO it would be nice to write:

let v = run_experiment();
let result = if v in 0.0...10.0 {
    Ok("class 1")
...

Personally I’d rather write:

let v = run_experiment();
let result = if 0.0 <= v <= 10.0 {
    Ok("class 1")
...

But ternary operators are just another layer of complexity. I can live with the language as-is.