- Feature Name:
postfix-match
- Start Date: 2022-07-10
- RFC PR: rust-lang/rfcs#0000
- Rust Issue: rust-lang/rust#0000
An alternative postfix syntax for match expressions that allows for interspersing match statements with function chains
foo.bar().baz.match {
_ => {}
}
as syntax sugar for
match foo.bar().baz {
_ => {}
}
Method chaining is something rust users do a lot to provide a nice flow of data from left-to-right/top-to-bottom which promotes a very natural reading order of the code.
Sometimes, these method chains can become quite terse for the sake of composability
For instance, we have the surprising ordering
of the methods like
map_or_else
.
This RFC proposes promoting the use of match statements by supporting postfix-match, reducing the use of some of these terse methods and potentially confusing method chains.
match expressions
are how one would normally deal with the values
of an enum type (eg Option
, Result
, etc.) by pattern matching on each
variant. Some powerful techniques like pattern binding, nested patterns and or patterns
allow for some versatile and concise, but still fairly readable syntax for dealing
with these types.
Rust often features functional approaches to lots of problems. For instance,
it's very common to have chains of map
, and_then
, ok_or
, unwrap
to process some Option
or Result
type in a pipeline, rather than continuously reassigning to new variable bindings.
let x = Some(42);
let magic_number = x.map(|x| x * 5)
.and_then(NonZeroI32::new)
.ok_or("5x was zero")
.unwrap();
Some of these provided method chains are fairly readable, like the ones presented above, but sometimes the desire to use long method chains is met with unwieldy hacks or awkward function arguments.
let x = Some("crustaceans");
x.and_then(|x| (!x.is_empty()).then(x)) // None if x is an empty string
.map_or_else(
|| "Ferris", // default
|x| &x[1..], // remove first letter
);
These can be re-written using postfix match to be much more self-documenting
x.match {
Some("") | None => None
x @ Some(_) => x
}.match {
Some(x) => &x[1..],
None => "Ferris",
};
// or even just
x.match {
Some("") | None => "Ferris"
x @ Some(_) => &x[1..]
};
While this example ended up being a single match, and is near-equivalent to the match x {}
form, often these option chains can get quite long, especially when interspersed with ?
, .await
and other forms of postfix syntax we already make heavy use of.
you could imagine that this x
would be replaced with
context.client
.post("https://example.com/crabs")
.body("favourite crab?")
.send()
.await?
.json::<Option<String>>()
.await?
.as_ref()
// prefix match
match context.client
.post("https://example.com/crabs")
.body("favourite crab?")
.send()
.await?
.json::<Option<String>>()
.await?
.as_ref()
{
Some("") | None => "Ferris"
x @ Some(_) => &x[1..]
};
// postfix match
context.client
.post("https://example.com/crabs")
.body("favourite crab?")
.send()
.await?
.json::<Option<String>>()
.await?
.as_ref()
.match {
Some("") | None => "Ferris"
x @ Some(_) => &x[1..]
};
While it's a matter of taste, the postfix form is much easier to parse
in terms of the flow of data. Having to jump back out to the beginning of
the expression to see the match
keyword to know what the new expression context is
can be difficult to read.
While I am not at liberty to share the codebase, the former is equivalent to something I see on some of our codebases at work.
While postfix match with a single match arm can replicate a pipeline operator, this should be actively condemned for a more well defined syntax or method actually intended for this purpose.
We already have a clippy lint clippy::match_single_binding
that covers this case
This avoids the need for a new dedicated pipeline operator or syntax.
One thing of note is that in option chains you cannot use futures unless you use adapters
like OptionFuture
.
Using match, you can avoid that by supporting .await
directly.
context.client
.post("https://example.com/crabs")
.body("favourite crab?")
.send()
.await
.match {
Err(_) => Ok("Ferris"),
Ok(resp) => resp.json::<String>().await,
// this works in a postfix-match ^^^^^^
}
X.Y.match {}.Z
Will be interpreted as
match X.Y {}.Z`
And this will be the same precedence as await
for consistency with all .
based prefix operators and methods we have.
Having multiple forms of match could be confusing. However, I believe that most issues could be resolved if clippy would detect uses of
x.match {}
when the desugared
match x {}
would fit on a single line after being rustfmt. The opposite could also be true - if the scrutinee spans multiple lines, it should be made into a postfix form.
The core rationale is that a lot of these method chain functions are designed to avoid using bulky match statements that ruin the flow.
Rather than keep adding more of these methods to suit the needs, we should make the language more flexible such that match statements aren't a hindrance.
*
and &
are very unfortunately low priority prefix operators in our syntax grammar.
If .match
has a higher prefix, this can be a little confusing as shown below
*foo.match {
Ok(3) => …,
Ok(100) => …,
_ => …,
}
where this is parsed as
*(match foo {
Ok(3) => …,
Ok(100) => …,
_ => …,
})
C# has a postfix switch expression where switch
has a lower precedence than methods and deref. This is something we could explore, not if we use .match
as the syntax.
postfix macros have been an idea for many years now. If they were to land, this feature could easily be implemented as a macro:
macro match_! (
{ $self:expr; $(
$arm:pat => $body:expr,
)+ } => {
match $self { $(
$arm => $body,
)+ }
}
)
However, after years of discussion and hundreds of thumbs-ups, it feels like we're still not close to agreeing on syntax or behaviour.
I've already mentioned tap for how we can do prefix-in-postfix,
so we could promote the use of .pipe()
instead. However, using the pipe method makes
async awkward and control flow impossible.
x.pipe(|x| async {
match x {
Err(_) => Ok("Ferris"),
Ok(resp) => resp.json::<String>().await,
}
}).await
We could add a new builtin pipeline operator
(e.g. |>
0 or .let x {}
>1)
but this is brand new syntax and requires a whole new set of discussions.
await
was initially proposed to be a prefix keyword.
There was a suggestion to make it postfix for very similar reasons (not breaking up method chains).
This eventually became the favourite given the pain that await chains introduces in other languages. I've heard many accounts from people that postfix-await is one of their favourite features of the language.
Method call chains will not lifetime extend their arguments. Match statements, however, are notorious for having lifetime extension. It is currently unclear if promoting these use-cases of match would cause more subtle bugs, or if it's negligible
In theory, some other Rust constructs could also have postfix alternatives, but this RFC should not be taken as precedent for doing so. This RFC only proposes postfix match
.