Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Postfix match #3295

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
313 changes: 313 additions & 0 deletions text/0000-postfix-match.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
- Feature Name: `postfix-match`
- Start Date: 2022-07-10
- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000)
- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000)

# Summary
[summary]: #summary

An alternative postfix syntax for match expressions that allows for interspersing match statements with function chains

```rust
foo.bar().baz.match {
_ => {}
}
```

as syntax sugar for

```rust
match foo.bar().baz {
_ => {}
}
```

# Motivation
[motivation]: #motivation

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](https://github.com/rust-lang/rfcs/issues/1025)
of the methods like
[`map_or_else`](https://doc.rust-lang.org/std/result/enum.Result.html#method.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.

# Guide-level explanation
[guide-level-explanation]: #guide-level-explanation

`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.

```rust
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.

```rust
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

```rust
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..]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this should be

    Some(x) => &x[1..]

Same mistake appears two more times later in the RFC.

Also previous line lacks , at the end of line (and three more occurrences).

};
```

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

```rust
context.client
.post("https://example.com/crabs")
.body("favourite crab?")
.send()
.await?
.json::<Option<String>>()
.await?
.as_ref()
```

```rust
// 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.

## tap (pipelines)
[guide-level-explanation]: #tap-pipelines

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`](https://rust-lang.github.io/rust-clippy/master/index.html#match_single_binding) that covers this case

This avoids the need for a new dedicated pipeline operator or syntax.

## async support

One thing of note is that in option chains you cannot use futures unless you use adapters
like [`OptionFuture`](https://docs.rs/futures/0.3.21/futures/future/struct.OptionFuture.html).

Using match, you can avoid that by supporting `.await` directly.

```rust
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 ^^^^^^
}
```

# Reference-level explanation
[reference-level-explanation]: #reference-level-explanation

`X.Y.match {}.Z`

Will be interpreted as

```rust
match X.Y {}.Z`
```

And this will be the same precedence as `await` for consistency with all `.` based prefix operators and methods we have.

# Drawbacks
[drawbacks]: #drawbacks

Having multiple forms of match could be confusing. However, I believe
that most issues could be resolved if clippy would detect uses of

```rust
x.match {}
```

when the desugared

```rust
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.

# Rationale and alternatives
[rationale-and-alternatives]: #rationale-and-alternatives

## Rationale

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.

## Alternatives

### Precedence

`*` 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

```rust
*foo.match {
Ok(3) => …,
Ok(100) => …,
_ => …,
}
```

where this is parsed as

```rust
*(match foo {
Ok(3) => …,
Ok(100) => …,
_ => …,
})
```

C# has a [postfix switch expression](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/#operator-precedence) 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

[postfix macros](https://github.com/rust-lang/rfcs/pull/2442) have been an idea for many years now.
If they were to land, this feature could easily be implemented as a macro:

```rust
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.

### Pipes

I've already mentioned [tap](#tap-pipelines) 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.

```rust
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. `|>`<sup>[0]</sup> or `.let x {}`><sup>[1]</sup>)
but this is brand new syntax and requires a whole new set of discussions.

[0]: https://internals.rust-lang.org/t/idea-operator-for-postfix-calls-to-macros-and-free-functions/7634
[1]: https://internals.rust-lang.org/t/idea-postfix-let-as-basis-for-postfix-macros/12438

# Prior art
[prior-art]: #prior-art
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something else you could add here:

C# has had C- and Java-style switch (foo) { … } since the very beginning. But C# 8.0 added postfix switch expressions that use foo switch { … } instead. That's very much like how Rust has had match foo { … } since the beginning while this RFC proposes adding foo.match { … }.

It's fun to see just how rusty that feature is. To use the example from the csharp_style_prefer_switch_expression style rule (roughly the equivalent of a warn-by-default clippy lint), instead of

switch (x)
{
    case 1:
        return 1 * 1;
    case 2:
        return 2 * 2;
    default:
        return 0;
}

It allows doing

return x switch
{
    1 => 1 * 1,
    2 => 2 * 2,
    _ => 0,
};

Using => and _ exactly the same way Rust does here. (Not that Rust invented it; it's also the same as many other languages.)

This isn't a perfect analog, since C#'s change does include a bunch of pattern syntax cleanup. But it's still another language in which it was decided to add a postfix matching operator even though everything it does could still be done (sometimes less nicely) with the existing prefix matching operator.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes of course! I even use them all the time at work 😅

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In C# the non-postfix switch is not an expression, in Rust it is.


## `await`

`await` was initially proposed to be a prefix keyword.

There was a [suggestion to make it postfix](https://github.com/rust-lang/rfcs/pull/2394#discussion_r179929778) 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.

# Unresolved questions
[unresolved-questions]: #unresolved-questions

## Lifetime extension

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](https://fasterthanli.me/articles/a-rust-match-made-in-hell#my-actual-bug), or if it's negligible

# Future possibilities
[future-possibilities]: #future-possibilities

I can't think of anything related to match that could be improved in this area
conradludgate marked this conversation as resolved.
Show resolved Hide resolved