Do we have support from Rust type system to do pattern matching?

I have to admit that this is a really wired (and maybe unclear) question, so let me explain it further with two examples adapted from Rust By Example.

In the Example 1, we have a const value in the first arm and ranges in the second and third arms of match, which actually implies comparability of a type (in this case i32). And we know that to express comparability in Rust, we need to impl traits like PartialOrd, Eq etc.

So, a sub-question is when we write pattern matching like Example 1, do we need to care about the traits like PartialOrd etc.? From another perspective, how can we do pattern matching like Example 1 using our own types?

// 1
fn main() {
    println!("Tell me what type of person you are");
    match 15 {
        0             => println!("I haven't celebrated my first birthday yet"),
        n @ 1  ..= 12 => println!("I'm a child of age {:?}", n),
        n @ 13 ..= 19 => println!("I'm a teen of age {:?}", n),
        n             => println!("I'm an old person of age {:?}", n),
    }
}

The above questions come up actually when I looked at Example 2. For temperature, we don't really need an enum but a float number, since the IS unit of temperature is Kelvin after all. If I want a type Temperature, I will make it comparable and I definitely would like to do pattern matching like in Example 1 (yeah.... floating point imprecision is too annoying to have full comparability).

// 2
enum Temperature {
    Celsius(i32),
    Farenheit(i32),
}

fn main() {
    let temperature = Temperature::Celsius(35);
    match temperature {
        Temperature::Celsius(t) if t > 30 => println!("{}C is above 30 Celsius", t),
        Temperature::Celsius(t) => println!("{}C is below 30 Celsius", t),
        Temperature::Farenheit(t) if t > 86 => println!("{}F is above 86 Farenheit", t),
        Temperature::Farenheit(t) => println!("{}F is below 86 Farenheit", t),
    }
}

Following Example 2, if I want guarding and ranging in pattern matching for my Temperature type, can I do that? If I can, do I need to impl traits like PartialOrd?

Below is my idealized code, please bear with me.

pub struct Temperature(u32); // u32 in Kelvin, just for demonstration purpose, bear with me please
const THRESHOLD_HOT: Temperature = Temperature::celsius(40); // suppose we have const fn celsius()
const THRESHOLD_COLD: Temperature = Temperature:: farenheit(40); // suppose we have const fn farenheit()

// any impls for Temperature needed??

fn main() {
    let t = Temperature::celsius(35);
    match t {
        hot_t if temperature > THRESHOLD_HOT => println!("TOO HOT!"),
        acceptable_t @ THRESHOLD_COLD .. THRESHOLD_HOT / 2 => println!("acceptable"),
        comfy_t @ THRESHOLD_HOT / 2 ..= THRESHOLD_HOT => println!("comfortable"),
        cold_t if temperature < THRESHOLD_COLD => println!("TOO COLD!"),
    }
}

The example in Destructuring array/slice is also interesting if you want to somehow destructuring your own array types.

All in all, I guess the conclusive question will be: Can we fit pattern matching into Rust type system?

I check the Patterns chapter of Rust reference and it says limited types are allowed in range pattern, so I guess there's been some thinking, but I don't know whether there's an RFC or something like that.

If you know something related, please let me know. Thanks!

No, or at least, it would require a big overhaul of the compiler's internals and a thorough RFC process. Pattern matching is currently a special process implemented in the compiler that is "structural" - it depends on the way you've defined your type and isn't affected by writing code or implementing traits.

The compiler is hard-coded to know how to reason about things like 1..=12 patterns so it can do exhaustiveness checks. However, things like if-guards don't factor into this exhaustiveness checking and there is no .partial_cmp() call that you can use to implement the acceptable_t @ COLD..HOT range check, so the code you are proposing won't work.

You might be able to do something like this, though:

enum Temperature {
    Celsius(i32),
    Farenheit(i32),
}

fn main() {
    let temperature = Temperature::Celsius(35);
    match temperature {
        Temperature::Celsius(t @ 30..) => println!("{}C is above 30 Celsius", t),
        Temperature::Celsius(t @ i32::MIN..=29) => println!("{}C is below 30 Celsius", t),
        Temperature::Farenheit(t @ 86..) => println!("{}F is above 86 Farenheit", t),
        Temperature::Farenheit(t @ i32::MIN..=85) => println!("{}F is below 86 Farenheit", t),
    }
}
1 Like

Also, we've put half-open range patterns on the path to stabilization, which would let Michael's example become

    match temperature {
        Temperature::Celsius(t @ ..30) => println!("{}C is below 30 Celsius", t),
        Temperature::Celsius(t @ 30..) => println!("{}C is above 30 Celsius", t),
        Temperature::Farenheit(t @ ..86) => println!("{}F is below 86 Farenheit", t),
        Temperature::Farenheit(t @ 86..) => println!("{}F is above 86 Farenheit", t),
    }

and avoid a bit of clutter from specifying the minimums directly.

4 Likes

Yeah, I use nightly for my daily driver so that's what my example originally looked like. Then had to change it because I realised it doesn't build on stable.

1 Like