Can I use a range inside if or while?


#1

Hi,

I can use a range inside match like this:

match char {
    '0'...'5' => tokens.push(Token::Foo),
    _ => tokens.push(Token::Bar),
}

Is it possible to use something similar for if or while? E.g.

while char == '0'...'5' {
}

This seems to throw. Is it because of this issue? What should I do instead? Write a helper function to use a match, but which returns a boolean?


#2

Ok, there’s a bit to unpack here.

First of all, match isn’t just using ==. It’s doing pattern matching, which allows you to do structural comparisons, or compare against multiple values.

Second, the things on the left of =>s in a match aren’t expressions, they’re patterns. Patterns look a lot like expressions, but aren’t, and have different rules. In particular, '0'...'5' is more or less just shorthand for '0' | '1' | '2' | '3' | '4' | '5', which means something totally different as an expression. Basically: don’t conflate the two.

Third, please don’t name variables after types. That’s just confusing.

Fourth, be really careful about using ranges of chars, because it doesn’t really make any sense outside of ASCII letters and numbers.

Fifth, the compiler doesn’t “throw”. Nothing in the language throws; Rust doesn’t have exceptions. That’s a compile error. I’m just a little worried you might be under the impression Rust is interpreted or something.

Sixth, in the case of char == '0'...'5' the problem is that you’re now talking about inclusive ranges in an expression context, and those aren’t stabilised yet. Basically, the core team hasn’t decided if they’re going to work like that. You can use exclusive ranges, though, which look like '0'..'6'. However…

Seventh, that comparison doesn’t make any sense. A char can’t be equal to a range of chars any more than 3 is equal to vec![1, 2, 3, 4, 5]. What you want is to ask if char is contained within the range '0'..'6', which you can work out once you realise that .. in expressions is just shorthand for constructing the values of the various std::ops::Range* types. In this specific case, it’d be a value of type Range<_>. This tells you there’s a Range::contains method… but that’s also unstable. So you’d need to do the comparison by hand by comparing char to the range’s start and end fields, and at that point, you might as well just write it out as a normal comparison.

The simplest way to do this is probably to use something like the matches crate, which provides a macro that lets you use the same match pattern you’re already using, but in an expression context, and returns a bool.

By the way, you should be careful with char range expressions too, because as I said, they don’t really make much sense. Whilst you can construct a Range<char>, you can’t iterate over it due to char not defining Step or Add.


#3

Thank you for the explanation so far.

First, second: Okay, this was probably the reason for my wrong assumption. I mean I can do match char { '(' => tokens.push(Token::ParenOpening), ... just like I can do if char == '('. I thought it would behave just the same not that there are differences, because of multiple values.

Third: Sorry, I thought the type was called Char, not char. My bad, but it was just an example…

Fourth: What would you do instead? I can only find examples which do that at some point to parse strings (e.g. here).

Fifth: Sorry, I didn’t meant that in a strict technical sense just as in “the compiler encountered an error”. I’ll look out not to use the term “throw” related to Rust code. I know Rust isn’t interpreted.

Seventh: Basically like first and second. I thought using a range here would imply something like the contains method. Not that I want to check if it is a range.


#4

Maybe interesting for others (even though it is probably not the best style in the world)… this works :wink:

while match c {
    '0'...'5' => true,
    _ => false,
} {
// do stuff
}

#5

You can use if let and while let like that:

if let '0'...'5' = c {
    // do conditional stuff
}

while let '0'...'5' = c {
    // do loopy stuff
}

Which basically desugars to the same while match that you wrote.


#6

I am thoroughly confounded at the fact that rust even has character range patterns.


#7

Yeah, it’s not a technical issue, it just looks really wonky, and messes up my mental parser.

For simple cases? Nothing. Like I said, it’s fine for ASCII letters and numbers, but beyond that… *grimaces* At that point, you’re probably better off just listing all the characters of interest, and letting the optimiser make it fast.

Just making sure.

Rust really doesn’t like people trying to be “clever”. Comparing a T to a Range<T> doing a contains check is “clever”. Rust has a general attitude of clearly stating what you want, even if it means being more verbose. Rust does have some magic, but you should set your expectations for this to be a pleasant surprise, rather than the expected norm.

When I do this, I put the condition inside a block, so it’s a little clearer what’s going on, and it looks better with indentation:

while {
    match c {
        '0'...'5' => true,
        _ => false,
    }
} {
    // do stuff
}

Yeah, I did a double take when I saw that. Which is weird, because I know I’ve used them before. I think they’re one of those things you compartmentalise as being the obvious, valid thing in a very narrow set of circumstances, but invalid and dumb everywhere else.