A step beyond Rust's pattern matching
radiki.dev
radiki.dev
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.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)I think simple pattern matching is useful, especially type-based pattern matching, including which element of a sum type is matched. But you want to watch out for how complicated it gets on the matching side. Complex matching code is a very strong form of coupling. It may be a good thing but I think as a language designer you don't want to overpower it because of the coupling it introduces as the power gets used.
https://en.wikipedia.org/wiki/Unification_(computer_science)...
Some language willing to trade a bit of speed for expressivity could adopt it. But not 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...
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.The main issue is that it's very hard to propagate information found through matching .
https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html#-...
In following code, it would be nice if the second match could use the early return from the first match to avoid extra checks. It would also be nice if it worked across function boundaries. It would help removing boilerplate conversions or some `unreachable!()` branches.
enum Auth { Guest, User(u32), Bot(u32) }
fn check_permissions(Auth &auth) -> Result<Permission, PermissionError> {
match auth {
Auth::Guest => return PermissionError::Unauthorized,
Auth::User(_) | Auth::Bot(_) => { ... }
// ...
match auth {
Auth::User(_) => {...}
Auth::Bot(_) => {...}
// At this point the compiler should know that `Auth::Guest` is not possible
} enum Auth { Guest, User(u32), Bot(u32) }
was just syntactic sugar for struct Auth::Guest;
struct Auth::User(u32);
struct Auth::Bot(u32);
type Auth = Auth::Guest | Auth::User | Auth::Bot;
where `|` denotes an anonymous union. Using enums feel very clunky when you start needing to work with subsets of them.The pattern type work I mentioned earlier would bring us very close to what you have at the end:
enum Auth { Guest, User(u32), Bot(u32) }
type FullAuth = pattern_type!(Auth, Auth::Guest | Auth::User(_) | Auth::Bot(_)); // useless, but works
type IdAuth = pattern_type!(Auth, Auth::User(_) | Auth::Bot(_));
The full enum still needs to be defined. In particular, it serves as the basis the in-memory representation. All the subset can be handled by the compiler though. 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?The former can't be restricted to type analysis (without something like dependent types I think?). You're right that we could apply constant folding to check in this case. My understanding is that the Rust design philosophy is to skip things like this because of the potential to produce surprising results upon refactoring.
> the potential to produce surprising results
That’s actually the big reason for that capability. If you do
let result: Result<_,!> = …
let ok(fine) = result
, if result ever changes from infallible to fallible, you get a compile error rather than the runtime error you would get with unwrap. In fact, when looking it up, it turns out that specific case was a big part of the motivation for the “!” type in the first place! I think the above code probably compiles in nightly because of that.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)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?
while let Some((a, a + 1)) = iterator.next() {
println!("({}, {})", a, b);
} if let (a, 2) = (1, 2) &&
let (c, 4) = (3, 4) && // should be b, not c
b == a + 2 {
println!("Made it, a = {}, b = {}", a, b);
} if let (a, 2) = (1, 2) &&
let (b, 4) = (3, 4) && // <-- This should be an open brace.
println!("Made it, a = {}, b = {}", a, b);
}