Ergonomic opposite of `matches!`

This post is more a problem statement than a proposed solution, but I'm curious what people think.

std::matches is a very useful macro. However, it can be a bit awkward when you want to branch on something NOT matching:

if !matches!(val, pattern) {
    ...
}

One alternative is to use a match expression, but that's exactly what matches! is designed to simplify.

Another is to use a let binding:

let is_match = matches!(val, pattern);
if !is_match {
   ...
}

...but that's a lengthy workaround for the awkwardness of the !matches! syntax.

Below are some spitball proposals for solving this problem.

Macro providing the opposite of matches!

Unfortunately there's not an immediately obvious name to me so it seems like mega bikeshed territory.

Some unenthusiastic proposals:

  • not_matches!
  • mismatches!

Pie-in-the-sky new first-class branching keyword: unless

An unless keyword sure would be handy here, but I know a lot of people dislike the idea because it's easily abused to write unclear backwards-logic unless used in very specific circumstances where it's clearer, such as this one.

Still, I think it'd be my preferred solution to this problem, and would generally be nice whenever you want the opposite of a macro that returns a bool:

unless matches!(val, pattern) {
    ...
}
3 Likes

Yet another alternative is to use else.

// rustfmt-style
if matches!(val, pattern) {
} else {
    ...
}
// single-line
if matches!(val, pattern) {} else {
    ...
}

Edit: Or how about just adding some parentheses:

// yes, rustfmt actually doesn’t eat these
// and clippy doesn’t complain either
if !(matches!(val, pattern)) {
    ...
}

Yet another alternative, as pointed out in this prelude discussion (which also mentions a previous internals thread):

use std::ops::Not;
if matches!(val, pattern).not() {
   ...
}

Perl and Ruby have this and I do agree it is handy in the right circumstances. But I also feel unless blocks become a liability due to confusion once combined with else blocks/chains. It could be restricted to the non-chained case, thought that would likely make it harder to justify a new keyword.

7 Likes

I agree that !matches! looks silly, but it feels unlikely to me that something else would overcome the "additional thing to learn" hurdle. We don't even have .is_not_empty() on slices, for example.

Agreed, but I think there's a way to keep that but give it value to the reader: force the body to be diverging. That way as soon as you see the unless keyword you know it's a precondition check, simple early return, etc. And in those the negation of the logic is actually helpful, since the condition is what's true after the statement, just like with assert!(x > 0);.

7 Likes

Yes, please, .not() on bool suits the language patterns in a very ergonomic manner, so feel free to add some support on that prelude suggestion: while matches! is currently the main pervasive macro used in condition position, there may be more in the future, and we should have a generalized way to avoid writing !something!(…) code, or having to define the negation of each and every method / macro in existence (not_something!, etc.)

While prefix ! has the "advantage" of being less surprising for people used to other languages1, and while it can be made less unreadable by using a space after it: if ! matches!(…), we will always have the problem of a condition using a method chain:

if !base.method1(…)
        .method2(…)
        .method3(…)
        .contains(…)
{
    …
}

vs.

if base.method1(…)
       .method2(…)
       .method3(…)
       .contains(…)
       .not()
{
    …
}

1I am personally not a fan of that argument, since it leads to repeating errors from the past, but I recognize that showcasing too many syntax differences within a language must be avoided, at least when that language starts. But not only is Rust now a well-known language, it would also be unfair to qualify .not() as a bizarre syntax.

3 Likes

Recently I had to write the following:

assert!(!matches!(peer, tl::enums::Peer::Channel(_)));

It does get pretty funky really soon! But I think it isn't too bad. Might be easy to miss accidentally during code review, but otherwise the syntax is using what everybody knows. The not() option was also suggested earlier, but I'm not a fan of it because the negation is not where I expect it to be.

2 Likes

How about

assert!(bool::not(matches!(peer, tl::enums::Peer::Channel(_))));

then?

4 Likes

That's a good alternative! A bit more verbose for sure, but definitely won't be overlooked on code review. However I'm going to stick with my funny looking approach for the time being ^^

assert_matches!(..) isn't useful when you want assert!( ! matches!( .. ) ). The correct single macro would be assert_not_matches! or maybe assert_not!(matches!(_)).

That it's possible to make the mistake of assuming you could go from assert!(!matches!( to assert_matches!( just goes to show how easy it is to miss that leading ! among the rest of the punctuation noise.

For the assert case specifically, I think it would make sense to have assert_not!(; it could provide a better error message than assert!(!.

1 Like

assert!(!...) seems like the easiest case for the new “magic assert” to detect and give a better error message for.

Ah, external dependencies, my nemesis :smiley:

I prefer to avoid them when possible, and if I can write assert!(! to avoid it I will. I'm sure the crate is very useful if you make extensive use of that or other of its macros though.

Wow the linked RFC is pretty cool and I had no idea it existed, I would love to see that. It seems there's no prior art section in the RFC, but for example, pytest does this too:

$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collected 1 item

test_sample.py F                                                     [100%]

================================= FAILURES =================================
_______________________________ test_answer ________________________________

    def test_answer():
>       assert inc(3) == 5
E       assert 4 == 5
E        +  where 4 = inc(3)

test_sample.py:6: AssertionError
========================= short test summary info ==========================
FAILED test_sample.py::test_answer - assert 4 == 5
============================ 1 failed in 0.12s =============================
2 Likes

I think it's more that when I typed "assert_mat" in my browser that's the link I got, and said "good enough".

I prefer not_matches!, unmatch!, or matches!() == false.

This will never land in Rust, in any shape or form. That's because it's really a cruft for idealists, not a solution to a real problem, and I am talking about both not_matches and unless. Both are entirely superfluous and don't really improve source code readability, as with them you now have two ways of negation - ! and not_/unless. Negation is too fundamental and simple a concept to have multiple ways of doing it. When considering a language change, make sure you test it in some way before suggesting it. For example, go on, write a not_matches macro, replace all of your !matches with not_matches, compare the sources and ask the following questions:

  • Does it really improve readability that much?
  • Was it that hard to write the macro by yourself?
  • Can you improve the situation in some other way? Like using an IDE with proper highlighting of macro calls that is visually distinct from operators.
6 Likes

I think @burjui has it right in terms of what the pragmatic (non-) solution is to this. !matches!(x, y) is not great as an expression, but it's a general problem with all macros that expand to boolean expressions - including cfg!() etc, and most solutions don't seem to be proportional.

I still couldn't resist putting in my thoughts though, but it's not with the goal of changing Rust, just musing. The following could be an idea:

let x = Some(1);
if matches!(x, not Some(1)) {
} 

The negation is moved into the pattern. matches!(x, not Some(1)) is equivalent to !matches!(x, Some(1)). This is readable but it has the drawback that a keyword like not has no precedent. And to be consistent, the same keyword should be usable in if let and matches as well - and have we ever seen utility for it there?

Matches example below. This doesn't feel easy to reason about at all, not a great feature :slight_smile:

match (1, Some(2)) {
    not (1, _) => {}
    (1, None) => {}
    (1, Some(_)) => {}
    // The three cases should cover it
}
3 Likes

I've hit a couple of situations where I thought it would be useful to be able to define patterns as items.

pattern<T> NOT_SOME: Option<T> = None;
pattern<T> SOME: Option<T> = Some(_);
pattern<T> SOME_1: Option<T> = Some(1);

If you could do that, you could write matches!(x, NOT_SOME) and have a reasonable way to share and document these things.

You can define "pattern aliases" using macros: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=24678e5e962a360b81cdbd394e6a84be

fn main(){
    macro_rules! not_some {
        ($($ty:ty)?) => { None $(::<$ty>)? }
    }

    if let not_some!() = None::<u32> {
        println!("Not Some");
    }
    
    if let not_some!(u32) = None {
        println!("Not Some");
    }

    macro_rules! tuple {
        ($($elem:pat),* $(,)?)=>{ ($($elem,)*) }
    }

    let tuple!(a, b) = (0, 1);
    println!("{} {}", a, b);
}

This obviously has all the downsides of macros, like having to write full paths inside the macro for #[macro_export]-ed macros, and macros being exported at the root of the crate.

Maybe not forever: https://github.com/rust-lang/rust/pull/78166

2 Likes

It didn't come to (my) mind before, but now that @bluss has mentioned them, negated patterns seem an "obviously" missing thing, the already mentioned caveats aside.

This is actually just a None pattern, not a not Some(value) pattern, which AFAICT cannot be easily expressed in a macro. E.g., a not Some(5) pattern would succeed on a Some(3) value as well.

Other than solving OP's problem statement, as already mentioned, they might be useful to filter out result patterns:

if matches!(val, not pattern) { ... }   // addressing OP's problem
if let res @ not Err(Fatal) = fn_returning_result() { /* res is type Result */ }
if let Err(err @ not Fatal) = fn_returning_result() { /* err is error type */ }

Exact syntax of course TBD, as well as how this would interact with match if guards and | arms.