More love for Rust pattern matching please

In a recent good blog post Graydon discusses about various possible future improvements for programming languages. A section is about “Richer patterns”. In that section Graydon seems to miss the very refined pattern matching of Mathematica ( http://reference.wolfram.com/language/tutorial/PatternsAndTransformationRules.html ).

I’ve found that in my Rust code the parts where I use pattern matching are often more bug-free than other parts of code. So I’d like more care and love for pattern matching in Rust (in match, if-let and function signatures). About this there are some enhancement requests, RFCs and bug reports, but the progress is very slow.

Features like slice_patterns and their advanced version are experimental still after two years. Code like this compiles (and this is silly in a reliable language like Rust):

fn main() {
    let x: u8 = 5;
    match x {
        0 ... 100 => (),
        100 ... 255 => (), // 100 covered two times
        _ => (), // required
    }
}

The compiler sometimes compiles the Less/Equal/Greater variants not efficiently (compared to if/then).

This is redundant and I’d like a shorter syntax:

struct Foo(u32);
fn bar(Foo(x): Foo) {}

Like:

fn bar(Foo(x): _) {}

There are situations where you want “if let … && …”, there’s even a crate to help this (yes, you can use if let on a tuple and this solves some cases, but not where the second item of the tuple must be what you have extracted in the first part).

Something like the Scala unapply() method could be handy.

5 Likes

I just wish to state for the record, that I consider developing Rust without clippy to be like buying a super duper new car with gazillions of safety features and then ignoring the beeping you get when not putting on your seatbelt:

warning: some ranges overlap
 --> src/main.rs:4:9
  |
4 |         0 ... 100 => (),
  |         ^^^^^^^^^
  |
  = note: #[warn(match_overlapping_arm)] on by default
note: overlaps with this
 --> src/main.rs:5:9
  |
5 |         100 ... 255 => (), // 100 covered two times
  |         ^^^^^^^^^^^
  = help: for further information visit https://github.com/Manishearth/rust-clippy/wiki#match_overlapping_arm
5 Likes

Seatbelts and the chime come with the car though, not a separate add-on :slight_smile: The more appropriate analogy is buying a super duper car with a navigation system that is less than stellar in getting you to some places, and using a 3rd party one instead.

The logical question is then why aren’t these warnings part of rustc itself?

4 Likes

We're in the process of getting this to be a component that all of your cars will have: https://github.com/rust-lang/rust/pull/43886

Because developing new lints or improving the existing ones is much faster out of tree. Also we don't want one gigantic monolithic compiler that does everything, but nice separate modular components.

1 Like

For me spotting duplicated patterns in a match is a job for the compiler errors.

Also Clippy can't fix the language (the need for the useless and bug-prone gotta-catch-all _=>).

2 Likes

Yup, I think that’s a fine adoption path. It’s just that your post sort of read as, because clippy lints this, it’s sort of ok :slight_smile:. Apologies if I misconstrued it.

You didn’t :wink: You completely read my post right. I think it’s a good idea to move some lints from clippy to rustc, but as long as the lint exists, the world is ok in my opinion.

But yea, we’re also trying to bundle clippy via rustup and on a stable compiler, which will blend the borders between separate tool and a component even further.

2 Likes

I’d like to see the numeric exhaustive match RFC revisited. I can’t find the link currently with GitHub being down, but essentially

use std::u8;

fn match_range(n: u8) {
    match n {
        0...u8::MAX => println!("{}", n),
    }
}

which clearly is an exhaustive match, fails to compile with

error[E0004]: non-exhaustive patterns: `_` not covered
 --> test.rs:4:11
  |
4 |     match n {
  |           ^ pattern `_` not covered

error: aborting due to previous error(s)

It was very surprising to me that the compiler couldn’t catch this.

2 Likes

I think about 5%-10% of Clippy lints should be moved into the core compiler (so the bugs get caught in case of code generation too, etc).

Probably the main factor that attracted me to Rust v.1.0 what seeing that it tried hard to do the right thing (like the handling of integer overflows, not implicit type conversions, and so on).

1 Like

Matching on Vecs inside other things is a problem:

#![feature(slice_patterns)]
#![allow(dead_code)]

fn foo1(a: Vec<i32>) {
    match a.as_slice() {
        &[] => (),
        &[_, ..] => (),
    }
}

fn foo2(a: Option<&[i32]>) {
    match a {
        None => (),
        Some(&[]) => (),
        Some(&[_, ..]) => (),
    }
}

fn foo3(a: Option<Vec<i32>>) {
    match a {
        None => (),
        Some(ref av) if av.is_empty() => (),
        Some(av) => println!("{:?}", av),
    }
}

fn main() {}

Scala unapply() standard methods could help: http://www.scala-lang.org/old/node/112

Allowing code similar to:

fn foo4(a: Option<Vec<i32>>) {
    match a {
        None => (),
        Some(&[]) => (),
        Some(&[_, ..]) => (),
    }
}
1 Like

One year later the situation has improved, Varkor and others have implemented the exhaustive integer patterns, usable in the last Nightly:

The implementation of #50912 was kind of exhausting, with 160+ messages and a lot of patience :slight_smile: It allows to write code like this, without the catch-all “_”:

#![feature(exclusive_range_pattern, exhaustive_integer_patterns)]

fn matcher1(x: u8) {
    match x {
        0 .. 32 => {},
        32 => {},
        33 ..= 255 => {},
    }
}

Currently Rustc allows overlapping arms, like this:

fn matcher2(x: u8) {
    match x {
        0 .. 50 => {},
        32 .. 60 => {},
        33 ..= 255 => {},
    }
}

You need the “match_overlapping_arm” default-warn Clippy lint to catch that possible mistake:

https://rust-lang-nursery.github.io/rust-clippy/master/index.html#match_overlapping_arm

Currently the type system is not able to handle code like this in a better way:

fn foo1(x: u32) {
    match x % 3 {
        0 => {},
        1 => {},
        _ => {},
    }
}

But I think that it’s possible to extend Varkor work with a expression value-range analysis to allow code like:

fn foo2(x: u32) {
    match x % 3 {
        0 => {},
        1 => {},
        2 => {},
    }
}

While supporting code like this could be left for later:

#![feature(euclidean_division)]
fn foo3(x: i32) {
    match x.mod_euc(2) {
        0 => {},
        1 => {},
    }
}

Lately some persons have suggested that ranged integrals could be added to Rust as library types (or even as a crate) once the Const Generics are implemented (worked on by Varkor again: https://github.com/rust-lang/rust/pull/53645 ). But I kind of think ranged integrals should be a language feature, because part of their value comes from their synergetic integration with several other language features (partially inspired by Ada features):

type Month = u8[1 ..= 12];
const DEC: Month = 12;

fn foo4() {
    for d in Month::INF ..= Month::SUP {
        match m {
            1 | 2 | 3 => println!("{}", 0),
            4 .. 12 => println!("{}", 1),
            DEC => println!("{}", 2),
        }
    }
}
6 Likes

There is now an accepted RFC to do just this; eRFC: if- and while-let-chains, take 2 by Centril · Pull Request #2497 · rust-lang/rfcs · GitHub

There is a pending proposal in P-FCP merge to do this, RFC: Generalized Type Ascription by Centril · Pull Request #2522 · rust-lang/rfcs · GitHub

Even better, the RFC would allow you to write:

fn bar(Foo(x)) {}
3 Likes

I don't think we can make an informed decision on that until we see how a library solution goes.

1 Like

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