Rust: Tracking issue for exclusive range patterns

Created on 18 Nov 2016  路  19Comments  路  Source: rust-lang/rust

Concerns

History and status

B-unstable C-tracking-issue F-exclusive_range_pattern T-lang

Most helpful comment

To consistent with for and other cases,

  • either we have to support both inclusive and exclusive patterns in match.
  • or either we should deprecate exclusive patterns in both places.
// 0 to 10 (10 exclusive)
for a in 0..10 { 
  println!("Current value : {}", a);
}

// 0 to 10 (10 inclusive)
for a in 0..=10 { 
  println!("Current value : {}", a);
}
let tshirt_width = 20;
let tshirt_size = match tshirt_width {
    16 => "S",       // 16
    17 | 18 => "M",  // 17 or 18
    19..21 => "L",   // 19 to 21; 21 excusive (19,20)
    21..=24 => "xL", // 21 to 24; 24 inclusive (21,22,23,24)
    25 => "XXL",
    _ => "Not Available",
};

While we having inconsistencies like this even in smallest things, it's more harder for newcomers to learn and master Rust.

All 19 comments

EDIT: the following is no longer true 12 jan 2019 nightly debug 2018:

Missing warning: unreachable pattern in the case when 1...10 then 1..10 are present in that order. That warning is present in case of 1..12 then 1..11 for example.


Example (playground link):

#![feature(exclusive_range_pattern)]
fn main() {
    let i = 9; //this hits whichever branch is first in the match without warning
    match i {
        1...10 => println!("hit inclusive"),
        1..10 => println!("hit exclusive"),//FIXME: there's no warning here
        // ^ can shuffle the above two, still no warnings.
        1...10 => println!("hit inclusive2"),//this does get warning: unreachable pattern
        1..10 => println!("hit exclusive2"),//this also gets warning: unreachable pattern

        _ => (),
    };

    //expecting a warning like in this:
    match i {
        1..12 => println!("hit 12"),
        1..11 => println!("hit 11"),//warning: unreachable pattern
        _ => (),
    }
}

I've just been saved for the second time by the compiler when I did 1 .. 10, but meant 1 ..= 10. It might be worth considering that the compiler won't catch this error if exclusive ranges are allowed.

seems like _exclusive range match_ would help to improve consistency ... why can i do range in a loop but not in a pattern?

 const MAXN:usize = 14;
 ... 
   for x in 0..MAXN  { blahblahblah(); }
 ...
   let s = match n {
      0..MAXN=>borkbork(),
      MAXN=>lastone(),
      _=>urgh(),
   };

also note that the following is not possible currently because the -1 will break the compile with "expected one of ::, =>, if, or | here"

   let s = match n {
      0..=MAXN-1=>borkbork(),
      MAXN=>lastone(),
      _=>urgh(),
   };

thank you

@donbright I know its a poor consolation, but a good workaround which ensures backwards compatibility might be:

        match i {
            0..=MAXN if i < MAXN => println!("Exclusive: {}", i),
            MAXN => println!("Limit: {}", i),
            _ => println!("Out of range: {}", i)
        }

playground

Figured this suggestion might help some poor soul who came here and also battled with not being allowed to use 0..=MAXN-1

It doesn't sound like much is going to happen on this issue, but (FWIW) I would argue against its inclusion in stable Rust: exclusive ranges on pattern matching makes it very easy to introduce bugs, as exclusive ranges are very rarely what the programmer wants in a pattern match (emphasis on match!). In these cases, I would argue that having the programmer be explicit is helpful; there is no reason to offer syntactic sugar in this case. (I landed here because of finding several bugs in a project that had turned this feature on; see https://github.com/tock/tock/issues/1544 for details.)

To consistent with for and other cases,

  • either we have to support both inclusive and exclusive patterns in match.
  • or either we should deprecate exclusive patterns in both places.
// 0 to 10 (10 exclusive)
for a in 0..10 { 
  println!("Current value : {}", a);
}

// 0 to 10 (10 inclusive)
for a in 0..=10 { 
  println!("Current value : {}", a);
}
let tshirt_width = 20;
let tshirt_size = match tshirt_width {
    16 => "S",       // 16
    17 | 18 => "M",  // 17 or 18
    19..21 => "L",   // 19 to 21; 21 excusive (19,20)
    21..=24 => "xL", // 21 to 24; 24 inclusive (21,22,23,24)
    25 => "XXL",
    _ => "Not Available",
};

While we having inconsistencies like this even in smallest things, it's more harder for newcomers to learn and master Rust.

ranges are very rarely what the programmer wants in a pattern match (emphasis on match!)

Arguments like that IMO are better grounds for lints, not "you can't have this".

Otherwise I'd be arguing you shouldn't be allowed to have a thing.is_file(), because it sometimes isn't what you expect: Usually you just want a readable thing that isn't a directory, and things like pipes and symbolic links and device nodes are acceptable, even if weird... but .is_file() will likely exclude those options for you. ( Hence why I proposed a lint for that )

And lets not even get into the rabbit warren of "stating files is bad, because you could have the underlying file change between the stat time and the time you modify it, use proper error handling instead" argument, because while I agree in part, its a decision you should make after understanding a lint telling you "why this is probably bad" and you decide whether or not it matters for your code.

If I may add a datapoint. I tried to use an exclusive range today and the compiler error took me here. Turned out I wanted an inclusive range, so I for one am greatful that rust does not (stabley) allow exclusive ranges and I would argue against stablising as it would have caused me to shoot myself in the foot. :)

I wanted exclusive ranges today, for a case where I had a variable that needed to be handled differently depending on which of the following ranges it fell into: 0..10, 10..100, 100..1000 and 1000..10000. In my opinion the code would have been cleaner if exclusive ranges were supported.

for a case where I had a variable that needed to be handled differently depending on which of the following ranges it fell into: 0..10, 10..100, 100..1000 and 1000..10000.

You could just do a match on x.log10().floor()

for a case where I had a variable that needed to be handled differently depending on which of the following ranges it fell into: 0..10, 10..100, 100..1000 and 1000..10000.

You could just do a match on x.log10().floor()

Yeah, good point! But at least to me, it wouldn't be as readable. The match would be something like:

0 => /* handle  case 0..10 */,
1 => /* handle case 10..100 */,
2 => /* handle case 100..1000 */,

Which isn't bad, but without the comment it wouldn't be immediately obvious that the case "0" handled the range 0..10 .

It is not clear that

0..=9
10..=99
100..=999
1000..=9999

is less desirable.

However I would like to voice that I prefer having the option to have an exclusive range pattern for the _sole case_ of the "half-open range" (#67264) that goes from a certain value unto the natural end of that range

@workingjubilee I agree that the difference in readability is not entirely obvious.

But let's say we had these ranges instead:

# Constants:
const NOMINAL_LOW:i32 = 0;
const NOMINAL_HIGH:i32 = 100;
const OVERVOLTAGE:i32 = 200;
# Ranges:
NOMINAL_LOW .. NOMINAL_HIGH,
NOMINAL_HIGH .. OVERVOLTAGE,

In this case, having to write NOMINAL_LOW..=NOMINAL_HIGH-1 seems slightly worse. than NOMINAL_LOW..NOMINAL_HIGH .

I know matching on floats is being phased out, but if that capability was intended to be kept, using inclusive ranges would have been quite difficult, since you couldn't just decrement by 1 to get the predecessor of NOMINAL_HIGH. Will rust ever support matching on user types? Like on a fixed point type, big number, or something like that?

How we can push this issue/fix forward?

I would prefer this to exist for consistency, and because I personally just find half-open ranges easier to reason about, even in match statements.

I believe many of the concerns listed could be addressed via the existing exhausiveness checking, and a clippy lint against using _ in combination with range patterns when an exhaustive pattern can be easily specified.

For example, instead of:

match x {
    0..9 => foo(),
    _ => bar(),
}

The lint could push you towards:

match x {
    0..9 => foo(),
    9.. => bar(), // Would fail to compile if you tried `10..` due to exhaustiveness checking
}

At which point it would be very obvious that perhaps you meant:

match x {
    0..10 => foo(),
    10.. => bar(),
}

That seems like a good idea, and I would actually say this almost shouldn't be a Clippy lint because it's fundamental to Rust's exhaustiveness capabilities, but it could be at least tested in Clippy.

I suppose there's a risk of false positives or arguments on style, and it making rustc too opinionated.
Exclusive and half-open range patterns definitely make more sense together rather than separately.

@Diggsey, I think that that's a great suggestion, though I would actually go a step further and have the compiler itself warn on non-exhaustive patterns (that is, use of _ when using exclusive range patterns) -- I think that one would much rather explicitly turn off the warning to allow any reader of the code to see what's going on. I know that my comment was very controversial, but I would also add that it was effectively coming from the field: it should be an important data point that a project enabled this feature, and it was rampantly misused, introducing (many) subtle bugs. Coming from C, I would liken exclusive range patterns to switch fallthrough: yes, there are cases where it's useful -- but it's so often done accidentally that the programmer is also required to mark it as /*FALLTHROUGH*/ to prevent lint from warning on it. One of Rust's great strengths is that it (generally) doesn't have trap doors like this; forcing the programmer to either use exhaustive pattern matching (which would have caught the bugs that I hit, for whatever it's worth) or explicitly turn off the warning seems like an excellent compromise!

"RangeFrom" patterns (X..) will be partially stabilized in #67264. No decision was reached regarding "RangeTo" (..Y) or "Range" patterns (X..Y).

Nearly four years, and it's still experimental. Sigh. Both inclusive and exclusive ranges are useful; when working with matches for odd usages of ASCII-like bytes (current: working on parsing the mess that is the DNS NAPTR service field), both inclusive and exclusive ranges may make code far more readable in different circumstances. Given that constant maths isn't possible in range patterns yet, it's a royal pain to make code clearly demonstrate its purpose with one arm tied behind your back. I'm all for a compiler and linter making it difficult to make mistakes, but, ultimately, good software requires comprehensive testing practices to go with it.

Good software needs to read like a good book; any choice about syntax, naming and the like should always be driven from that observation.

Was this page helpful?
0 / 5 - 0 ratings