TL;DR: Introduce patterns of the form f -> pattern
, which have the semantics of applying f
to the matched value and then matching pattern
on the result. This feature generalizes pattern synonyms, box_patterns
, advanced_slice_patterns
, etc.
Pattern matching is a very powerful construct. It makes it very easy to work with complex structures like nested slices, lists, and ASTs.
However, pattern matching in its current form still has many limitations. For example, there is no stable way to match the contents of Box
or an Rc
. There is no way to abstract common patterns, short of using a macro (which has its own limitations; for example, macro-generated patterns defined in a module foo
cannot match on private types from foo
– example).
It would also be nice if libraries could provide pattern-matching constructs other than what are built in via enums. For example, one might want a way to match a slice that starts with a certain prefix and then bind a variable to the sub-slice that comes after that prefix. There is currently no way to do this.
In short: patterns currently can’t build abstractions.
What I propose is that we add a feature analogous to Haskell’s view patterns (or ‘the one pattern to rule them all’), with a few modifications to account for the specialties of Rust’s borrow checker.
Example
As an example of where this might be useful, consider the following code:
fn to_digit(c: char) -> Option<u32> {
c.to_digit(10)
}
fn classify(c: char) -> CharClass {
match c {
' ' | '\t' | '\n' => CharClass::Whitespace,
c where c.is_ascii_alphabetic() => CharClass::Alpha,
c where to_digit(c).is_some()
=> CharClass::Digit(to_digit(c).unwrap()),
_ => CharClass::Other,
}
}
The third arm – the one that checks for digits – is quite ugly. With view patterns, it could be rewritten as:
/* ... */
fn classify(c: char) -> CharClass {
match c {
' ' | '\t' | '\n' => CharClass::Whitespace,
c where c.is_ascii_alphabetic() => CharClass::Alpha,
(to_digit -> Some(n)) => CharClass::Digit(n),
_ => CharClass::Other,
}
}
Interaction With Ownership
Rust distinguishes between patterns that move the value they match on and patterns that only borrow it. This is an important distinction because a construct like a @ subpattern
can only work if subpattern
doesn’t move the value it matches on.
Because of this distinction, we will need three different forms of view pattern: one where the view function takes its argument by value, on where it takes it by reference, and one by mutable reference. For this example, I’ll be using f -> p
, ref f -> p
, and ref mut f -> p
respectively, but this is open to change (in in fact probably should be).
To be precise:
-
if let (f -> pat) = v {}
is equivalent toif let pat = f(v) {}
. -
if let (ref f -> pat) = v {}
is equivalent toif let pat = f(&v) {}
. -
if let (ref mut f -> pat) = v {}
is equivalent toif let pat = f(&mut v) {}
.
Exhaustiveness
A view pattern such as f -> pat
should be considered irrefutable if and only if pat
is irrefutable. Otherwise, no assumptions should be made about whether this will match.
Pros
-
This allows one to express some constructs that cannot currently be written. The example code shown above cannot currently be written with a
match
without a redundantunwrap
becauseif let
guards are not supported and there’s no way to fallthrough to the next arm of amatch
. -
We can continue adding new pattern constructs such as range patterns, box patterns, advanced slice patterns, etc., but this feature subsumes all of them and also allows library authors to define their own.
-
Range patterns can be implemented using the following view function:
fn range<'a, B: Ord>(start: &'a B, end: &'a B) -> impl Fn(&B) -> bool + 'a { move |e| start <= e && e < end }
The pattern
a..b
becomes(ref range(&a, &b) -> true)
. -
box p
can be reduced to one of((|b| *b) -> p)
,(ref Deref::deref -> p)
, or(ref mut DerefMut::deref_mut -> p)
. -
advanced_slice_patterns
takes more work (it would require return-type polymorphism and more complex type-level hackery), but it could be done; more importantly, it could be done without adding anything to the compiler but this one feature.
-
Cons
- This adds more complexity to patterns.
- The feature is potentially confusing and hard to search for.
There are probably more problems with this that I haven’t thought of.
Bikeshedding
- Should the parentheses always be required around view patterns?
- I believe the current syntax would cause infinite lookahead, so we probably want some kind of mandatory prefix.
- It would be nice if there was a way to write a method call in place of
f
, or to otherwise include functions that don’t just take one parameter. You can use a lambda, but(|c| c.to_digit(10)) -> Some(n)
looks a bit ugly. - It might be more ergonomic to have the difference between
(f -> pat)
,(ref f -> pat)
, and(ref mut f -> pat)
be inferred instead of explicit. - Should we have special syntax for
f -> Some(p)
andf -> true
?