A step beyond Rust's pattern matching

PaulHoule | 108 points

In Rust, let-else patterns don't allow this, but match guards (used in `match` expressions and the `matches!` macro) can do something like this.

  while matches!(iterator.next(), Some((a, b)) if a + 1 == b) {
      println!("Hello!");
  }
However, using the bound values in an expression is a little harder.

  while match iterator.next() {
      Some((a, b)) if a + 1 == b => {
          println!("{a}, {b}");
          true
      },
      _ => false
  } {}
Since this is a well-formed transformation, it should theoretically be possible to implement this as a macro without changing the language.
claytonwramsey | a month ago

The problem with patterns allowed to be expressions that simply evaluate to an object to be matched is that this gets in the way of doing potentially more useful things, like predicates, so it's kind of a waste:

TXR Lisp:

  1> (match (@a @(= (+ a 1))) '(1 3) a)
  ** match: (@a @(= (+ a 1))) failed to match object (1 3)
  1> (match (@a @(= (+ a 1))) '(1 2) a)
  1
An expression is treated as a predicate. The object being matched, like 2, is inserted as a right argument into it: (= (+ a 1) <obj>).

If you try the following, you get a surprising result:

  2> (match (@a @(+ a 1)) '(1 3) a)
  1
It is more verbose, but we can do more. We don't have to test for exact equality; e.g. @(< a) will match something that is greater than the previously matched a.

Capture a b that is greater than previously captured a:

  3> (match (@a @(< a @b)) '(1 3) (list a b))
  (1 3)
Or fail trying:

  4> (match (@a @(< a @b)) '(1 0) (list a b))
  ** match: (@a @(< a @b)) failed to match object (1 0)
We could treat certain functions differently; since + isn't a relational function producing a Boolean result, equality could be inserted implicitly to generate a predicate.

But, rather than that hack, there is a different one: user-defined patterns. We can have @(+ a 1) as a pattern by defining + as a pattern operator:

  1> (defmatch + (. args) ^@(= (+ ,*args)))
  +
Then:

  2> (match (@a @(+ a 1)) '(1 2) a)
  1
  3> (match (@a @(+ a 1)) '(1 3) a)
  ** match: (@a @(+ a 1)) failed to match object (1 3)
kazinator | a month ago

So I think what the author is suggesting is what was called “n+k patterns” in Haskell. I don’t recall the specific history, but over time there was a strong concensus that it was a bad idea. This might have been related to how signed integers work semantically, or some other gotcha

carterschonwald | a month ago

Why don't languages who need pattern matching use Unification? Turing-complete pattern matching, with known fast implementations. Why does everyone need to reinvent the wheel ad-hoc, and end up with a square wheel that only rolls on Tuesdays?

https://en.wikipedia.org/wiki/Unification_(computer_science)...

YeGoblynQueenne | a month ago

> Opens up blog post about pattern matching in Rust.

> Not a single "match" statement.

This post could increase its reach by explaining why a let statement is pattern matching, or at least point to a reference.

Although I understand people who don't know this already are not the target audience.

Here's the reference for those who need it: https://doc.rust-lang.org/book/ch18-01-all-the-places-for-pa...

samueloph | a month ago

What I really love about Rusts pattern matching is how well it integrates with the type system.

    match user {
       ImportUser{id, account: {AccountData: email, ..}, ..
    } => println({id}: {Some(email)}),
        _ => println("Not an ImportUser or has no mail")
    }
Of course this specific case could also be written with `if let`, but imagine how long this specific case would have been using the traditional conditionals.
atoav | a month ago

This looks a lot like logic programming: https://en.wikipedia.org/wiki/Logic_programming

armchairhacker | a month ago

The case at the beginning, where we know our pattern is irrefutable but the compiler doesn't (let (x, 2) = (1, 1+1)) ,is something the compiler could easily prove (as long as the irrefutably could be proven locally as it is here). At first, I struggled to think of why that capability would ever be helpful. In the end, though, I came up with this example, which can (and does) come up:

  fn infallible() -> Result<u8, !>{
      Ok(1)
  }
  
  fn match_infallible(){
      let Ok(x) = infallible();
  }
The compiler here knows the `infallible` function will always return an `Ok` because of the return type. Here you could just `unwrap` the result, but what if it was inside a tuple, or something similar?
arijun | a month ago

I think this part of the article has a typo (it is missing (2, 2) and a comma after the tuple in the vector macro assignment)

Say I have a vector of pairs:

    let the_data = vec![
      (1, 2),
      (2, 2)
      (2, 3),
      (5, 7),
      (8, 8)];
I can grab an iterator out of it and do the same pattern matching in a for loop:

    for (a, b) in the_data {
        println!("({}, {})", a, b);
    }
    
    // prints
    // (1, 2)
    // (2, 3)
    // (5, 7)
    // (8, 8)
MuffinFlavored | a month ago

I'm not a fan of forcing a left-to-right order in the author's proposed syntax here. Sure the example given by OP is `while let Some((a, a + 1)) = iterator.next()` but if you want to write `while let Some((b - 1, b)) = iterator.next()`?

This is a contrived example because you can switch expressions to make it work. Now what if you want to match a tuple where the first element is the SHA256 hash of the second?

kccqzy | a month ago
[deleted]
| a month ago

There should be no `b` here in the println, this looks like a typo:

  while let Some((a, a + 1)) = iterator.next() {
    println!("({}, {})", a, b);
}
croemer | a month ago