Can you store range patterns as const?

Currently I'm using a lot of const for use in pattern matching. E.g.:

const MY_RANGE_START: u32 = 42;
const MY_RANGE_END: u32 = 211;
// ...
match value {
    MY_RANGE_START..=MY_RANGE_END => {/* ... */}
    _ => {/* ... */}
}

Is there a way to store the range directly and use that instead of having to write out the start and end each time?

1 Like

What you are looking for is RangeInclusive.
With this your code becomes:

use std::ops::RangeInclusive;
const RANGE: RangeInclusive<{Integer}> = MY_RANGE_START..=MY_RANGE_END;

match value {
    RANGE => {}
    _ => {}
}

I tried that but it doesn't seem a to work, unless I'm doing something wrong. Here's a minimal example on playground.

error[E0308]: mismatched types
expected integer, found struct std::ops::RangeInclusive

There is a type mismatch because you're trying to match an integer to a RangeInclusive object.

Here is a link to a working playground example. Unfortunately you are back to where you started and your original code is cleaner.

Frankly, I'm a little surprised that your first code worked! It looks like the compiler evaluates the pattern in the match statement to make the types work out. When you add the const I'm (totally) guessing it doesn't evaluate it any more.

Thanks. It's a shame I have to be so verbose but I'll see if I can reorganize my code to make it necessary in fewer places.

Intrigued by this, I looked a little further and found this from the rust reference online. Now I'm confused again. Matching on ranges is specifically covered, so why doesn't a const pattern work? All you're saying is that the pattern will never change. It seems like it should work. Does anyone more knowledgeable than me know, should we file this as a compiler bug? @OptimisticPeach?

Edit: I searched the rustc issues and couldn't find anything obvious, but compilers are way above my skill level.

Sorry for the delay, I believe I know why it doesn't work.
When you use x..y in a pattern, it is just that, a pattern, so when you do the following:

match value {
    x..y => {} 
    _ => {}
}

The compiler will explicitly match for values between x and y, meaning that it never actually constructs a RangeInclusive or Range, and instead actually matches for the range inline. Therefore, it doesn't work because you cannot match a type T to a type U.

Therefore you might be able to minimize your code to the following:

use std::ops::RangeInclusive;
const MY_RANGE_START: usize = 0;
const MY_RANGE_END: usize = 20;
const RANGE: RangeInclusive<usize> = MY_RANGE_START..=MY_RANGE_END;

fn main() {
    let value = 10;
    match value {
        x if RANGE.contains(&x) => {}
        _ => {}
    }
}

But, of course this is still somewhat verbose, so I'd still use actual ranges.

Please note, I am not very knowledgeable about compiler internals, so please take this with a grain of salt.

4 Likes

Just tested it on the playground and it works nicely. Thanks!

That is a bit verbose, but it's very clear what you're doing just from reading the code.

1 Like

Oof, that's unfortunate. If you have to do it frequently, I might try writing a helper function:

fn match_int(
    x: u32,
    f0: impl FnOnce(u32) -> T,
    f1: impl FnOnce(u32) -> T,
) -> T {
    match x {
        MY_RANGE_START..=MY_RANGE_END => f0(x),
        _ => f1(x),
    }
}

match_int(3,
    |x| println!("{}", x),
    |_| {},
);

Or if you have many ranges, use a struct to simulate named arguments:

struct MatchFuncs<FFoo, FBar, FOther> {
    foo: FFoo,
    bar: FBar,
    other: FOther,
}

fn match_int(x: u32, funcs: MatchFuncs<FFoo, FBar, FOther>)
where
    FFoo: FnOnce(u32) -> T,
    FBar: FnOnce(u32) -> T,
    FOther: FnOnce(u32) -> T,
) -> T {
    match x {
        FOOS_START..=FOOS_END => funcs.foo(x),
        BARS_START..=BARS_END => funcs.bar(x),
        _ => funcs.other(x),
    }
}

match_int(3, MatchFuncs {
    foo: |_| "foo",
    bar: |_| "bar",
    other: |_| "other",
});
3 Likes

Thanks OptimisticPeach and ExpHP. I'll experiment with different approaches and see what works best for my code.

Small tip: there's a generic function in prelude which is identical to |_| {}, called drop. :smiley:

1 Like

Yes, this is exactly what happens. In particular, the pattern x ..= y turns into hir::PatKind::Range.

To allow folks to abstract over range patterns we would need pattern synonyms.

3 Likes

Aaaah!! That reminds me!!!

Here is an unusual but legit solution: Macros can be used in pattern position!

const FOO_START: u32 = 0;
const FOO_END: u32 = BAR_START - 1;
const BAR_START: u32 = 15;
const BAR_END: u32 = BAZ_START - 1;
const BAZ_START: u32 = 24;
const BAZ_END: u32 = 32;

macro_rules! foo_range { () => (crate::FOO_START ..= crate::FOO_END); }
macro_rules! bar_range { () => (crate::BAR_START ..= crate::BAR_END); }
macro_rules! baz_range { () => (crate::BAZ_START ..= crate::BAZ_END); }

fn main() {
    match 17 {
        foo_range!() => println!("foo"),
        bar_range!() => println!("bar"),
        baz_range!() => println!("baz"),
        _ => println!("other"),
    }
}
4 Likes

I guess one of the problems here is the overloaded semantics of a..b and a..=b. You can use the syntax as a value constructor to create Range[Inclusive] objects, but when the same syntax is used as a pattern, it does not match or destructure Range[Inclusive] objects. Instead its meaning changes to checking whether an integer is in the given range.

Sure, but I think while it is a good goal that pattern matching should follow construction and vice versal (duals..) it isn't achievable in general. For example, the or-pattern A | B has no dual in expressions. We can only say they are duals for the specific case of bidirectional pattern synonyms (i.e. there's a bijection).

I've just been playing with this idea. Interestingly it breaks when using the or-pattern (|) centril mentioned. For example:

macro_rules! range { () => (0..=5 | 9..=13); }
fn main() {
    match 7 {
        range!() => { println!("in range") }
        _ => println!("other"),
    }
}

This gives the error: "macro expansion ignores token | and any following" and tells me the macro is likely invalid in the pattern context.

Indeed, | is not part of pattern syntax in general, but rather it is specifically part of match syntax.

(it was added to if let in 1.33.0, but it's still not considered to be part of pattern syntax proper)


Furthermore, because $(pat)|* is legal in match arms (i.e. | is in the follow-set of patterns), attempting to make it part of pattern syntax would be a pretty big breaking change. (this is in contrast to pat:ty, where such an extension was anticipated and : was not placed in the follow-set, enabling this RFC to exist)

2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.