-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
base: master
Are you sure you want to change the base?
RFC: Postfix match #3295
Conversation
15668a6
to
ce40102
Compare
My initial reaction after reading the title was that this was unnecessary. However, after thinking about it for longer, I realised how useful this actually is, and how many times this would've helped the code I'm writing. I think the RFC already justifies this feature quite well. I would just like to add two related points:
The conversion from
to
should probably not be attempted. From my point of view, function chains represent pipes where flow starts at the top, only moves down, and finishes at the bottom. The conversion from
to
is redundant. This is what iterators do. Also, same caveat about continuing the chain. Finally,
Currently I cannot, but postfix match allows
in function chains which is not as clean and simple as |
I tried to avoid mentioning any other postfix keywords because I don't want this RFC to turn into arguing about other postfix alternatives. However I understand it's always the natural progression "what's next?". Postfix for/while don't make sense to me since they can't currently evaluate to a value other than Postfix let isn't a terrible idea (also recently discussed as an Postfix Postfix ref/deref would be nice, but this is also doesn't seem to fit related to this proposal. Postfix |
That's a fair point. As far as this RFC is concerned, I'm fully in favor of it. And regardless of what decisions we make about those other postfix cases you mentioned, we don't need to worry about them now. Either they clearly don't make sense, or they are covered by this, or we can look into them later in their own RFCs if we feel like it without hampering the progress in this one. I think postfix match is one solid idea that stands on its own and doesn't necessarily require other postfix options to exist (or even be considered) as well. Plus, it already has the precedent of postfix |
Here's my previous thoughts on this: https://internals.rust-lang.org/t/half-baked-idea-postfix-monadic-unsafe/10186/11?u=scottmcm Basically, Rust is pretty good about "there's a warning up front" when things affect everything in a block, but that's not necessary when something only deals in the resulting value. That's why And thus But There are some other things that could work ok as postfix. For example, foo()
.zip(bar)
.whatever()
.for x {
call_something(x);
} meets all my requirements for where postfix would be fine (though that doesn't imply desirable). But procedurally I think considering things one at a time is for the best (unless they're deeply connected). |
An alternative I don't see mentioned yet is to use a // prefix match, but using `let` so that the `match` goes at the end
let favorite = context
.client
.post("https://example.com/crabs")
.body("favourite crab?")
.send()
.await?
.json::<Option<String>>()
.await?;
match favorite.as_ref() {
Some("") | None => "Ferris",
x @ Some(_) => &x[1..],
}; That works today, and puts the |
@sunfishcode I guess the argument there is that postfix |
Another advantage of postfix match is that it would reduce the need for more Option/Result helper methods. For example, it makes It also means that code that would need |
FWIW, I was playing with an implementation of this a while ago. It's rather simple to handle this entirely in the parser, although I didn't add stability-gating or anything like that. (edit: I just rebased and it still works!) |
Please note that all of the following is just my opinion. This is a request for comments, so I'm commenting from my perspective. Similar to @truppelito, my initial reaction was that this was unnecessary. Unfortunately after thinking about it on my own for a bit that is still my opinion. I don't feel like this feature is sufficiently motivated. I spend a fairly considerable amount of time in the rust community discord. If this were added, I see the following scenario playing out. Baby rustacean Cassy shows up and asks the question: "what's the difference between I feel dissatisfied with that answer. It is not immediately clear to me (or I imagine to budding rustacean Cassy) what the motivation for the difference is. At a glance, it seems like two ways to do the exact same thing. I think this is different from option/result utilities like But the thing I really dread is the follow-up question of "when should I use which?" or "which one is better?" After reading the RFC I don't think I could tell you a non-opinion-based answer. I think the proposal to have rustfmt choose it for you based on the span of the scrutinee is a reasonable idea, but I wouldn't be surprised if we start seeing stuff like "we only use prefix/postfix match in this repository" in contributing guidelines, which I don't think is good. I genuinely feel like the decision to use prefix/postfix match will end up being a matter of preference over utility the majority of the time. More on that point, if your chaining is so long that it wouldn't fit on a single line in a prefix match I'd argue you need to break that chain up. Function chains are great - I love them and write them all the time - but when I look at my old code after a few months I find I spend more time than I'd like figuring out what the intermediate values are. Taking this example from the RFC: context.client
.post("https://example.com/crabs")
.body("favourite crab?")
.send()
.await?
.json::<Option<String>>()
.await?
.as_ref() I have used HTTP libraries that look just like this but I found my self subconsciously asking "wait what is that second let response = context.client
.post("https://example.com/crabs")
.body("favourite crab?")
.send()
.await?;
let json = response.json::<Option<String>>()
.await?
.as_ref(); For instance, nothing in the original code snippet indicated that the expression evaluated to the response of the request, now that's fairly obvious for this particular example, but in the general case I think it's easy to get lost in the chains. I think that encouraging further extension of such chains with I would also like to note that I have never actually wanted or thought of having a feature like this before I saw this RFC today. When I saw this I didn't (and still don't) identify any problems it's solving for me. Now because this is my experience I don't feel like the small ergonomics improvements outweigh the duplication of a language feature, but your opinion may differ, of course. |
@truppelito careful, this will make a difference since the |
@T-Dark0 No one would be happy from that half-solution. Match, postfix or not, still involves a lot of boilerplate for standard combinators. People will want to eliminate it, so it's just a stopgap and redundant syntax. The only thing it would save from is giving an explicit name to the match scrutinee, which isn't a good enough reason to add new syntax. Overall, this RFC doesn't seem to be solving any practical issues, give any significant ergonomic or conciseness benefits. It's almost the same amount of code, apart from an explicit variable binding, which is more of a downside than a benefit. An explicit match scrutinee name helps to document the transformations for future readers. While the pipelining makes the dataflow slightly more obvious (slightly, since consecutive variable bindings give almost as much information, apart from a possibility of using the bindings in other places), it obscures the state of the pipelined variable. You know all the small step transformations, but to learn the state at any step you need to mentally simulate the entire pipeline up to that point. This means that long pipelines can only be read as an indivisible whole, and require keeping the entire pipeline in your head, which is a downside for readability. Currently, the I also wonder where should we stop if this RFC is accepted? The same reasoning can be applied to any other syntactic construct ( |
I don't understand this section talking about “Method call chains will not lifetime extend their arguments. Match statements, however, are notorious for having lifetime extension.” I thought that temporary scopes are essentially the same between the scrutinee of a |
Regarding code style, I think that in the case of "stacked" matches, a postfix match is likely to be more easily understood. Consider something like: match match some_enum {
E::A => F::A,
E::B => F::B,
E::C => F::B,
} {
F::A => "FA",
F::B => "FB",
} could be re-written as: some_enum
.match {
E::A => F::A,
E::B => F::B,
E::C => F::B,
}
.match {
F::A => "FA",
F::B => "FB",
} The first example is so ugly that the author would likely have the sense to separate the operations across multiple statements or abstract the matches into methods and write this logic as a method chain Prefix match places the operand in the middle and the result on the right, whereas postfix places the operand on the left and the result on the right, which strikes me as similar to the difference between I sympathize with the scenario described by Cassy in which a beginner might be confused by having two ways to use
|
In case of stacked match I almost always think it should be separated into two statements, and I don't think postfix match will solve that.
I think this is way too much churn than we can allow ourselves. And if both are here to stay, then this will indeed create a lot of confusion IMHO. |
I didn't investigate it too deeply, but I remembered that match had a seemingly unintuitive approach to temporaries. But if you're correct that it makes it more consistent with method chains, that's great for this RFC! |
[1]: https://internals.rust-lang.org/t/idea-postfix-let-as-basis-for-postfix-macros/12438 | ||
|
||
# Prior art | ||
[prior-art]: #prior-art |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 😅
There was a problem hiding this comment.
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.
Since postfix Current Basically, it's certainly true that there are various places where it might be a good idea to introduce a name. #3295 (comment) above mentions that in the context of method chaining and To me, the place for "hey, that chain got too long, can you break it up?" is code review, not the grammar. I don't think we need
See #3295 (comment) earlier where I discuss that in depth. No, the same reasoning cannot "be applied to any other syntactic construct". And even where it would be possible to do it, it does still need to pass a "is it worth bothering" check. For example, there's nothing fundamentally horrible about |
@steffahn Yes, of course 😂. This is a mistake I make regularly, but the compiler catches it for me. Naturally, here... there was no compiler. |
I don't claim that we should enforce that in the grammar. Perhaps postfix |
@ChayimFriedman2 "But I don't think it is worth to introduce a new way to do the same thing, just to be able to chain I understand and agree your point in general. Having two ways to do the exact same thing is probably unnecessary. Where we differ is that I consider the ability to chain I also consider using postfix
or
A precedent: I don't know what came first, Certainly, any time we write:
We can also write:
(I think this is true, correct me if not). So
I prefer However, if the |
Regarding the comments of: "naming the match scrutinee is a good thing and/or it should always be required" I agree, but not always. For example: @afetisov "Currently, the match expressions are minor sequence points, which require the code author to give some, hopefully descriptive, name to the state." I think we can apply the same clarity rules to both function chains and prefix/postfix In fact, YMMV but I find that
I find that the
which looks a bit redundant to me. Of course, the argument then is that not all |
(replying to a specific post because I was pinged, but I'm making these points in a general manner)
@afetisov No, of course not. I said alleviates, not solves, the problem.
This is only true on paper. 99% of code I've seen (even the snippets in this very thread!) doesn't add meaningful names for the intermediate "match variable". Instead, it names it after its type, or after the function it comes from. Hardly useful documentation. After all, can we expect a programmer to think about a good name when they didn't really want to introduce a name, and know that the variable is only used once, in the very next expression? Importantly, I think postfix match would make the situation better: if people aren't forced to add names by the grammar, the addition of a variable name becomes a deliberate process, done by choice. A programmer that chooses to name a variable is much more likely to take a moment to decide where the name should go and what it should be carefully.
Emphasis added. The pipelining makes the dataflow easier to read, and prevents readers from having to worry about other potential uses of the variable. Win-win.
Iterators work completely fine, and aren't generally accused of unreadability, and so do transformations like
This is also true if you use intermediate variables. Knowing the state of a variable at point X requires knowing how it was created, which requires knowing how the variables used in that computation was created, and so on backwards. let iter = vec![1, 2, 3].into_iter()
let iter = iter.map(|x| x + 1);
let iter = iter.filter(|x| x % 2 != 0)
//How would you know what the state of `iter` here is without reading the iterator "chain" that it comes from?
Emphasis added. This doesn't happen in reality.
It stops at
Forth had many great ideas. Concatenation was one of them. The real issue was that Forth only had concatenation and a stack to express things. Nobody is proposing to abolish variables, we won't become Forth. |
It could be argued that, unless we say "It stops at What makes this one so special that it gets privileged "here's where it stops" status as opposed to what comes next, or the one after that? ...because, a lot of rejected RFCs had a similar "Let's just add this one thing I want to the language and stop there. We don't need what other people want." feel to them. |
I just grep a few code that I have a copy of for some real world use cases which may be benefit by postfix match. (I did not include cases that are served better by combinators like
|
So I have read through all the comments, but I'm still not convinced by how this should help in:
Examples and missing objective reasonsI'd like to see some objective reasons, why postfix Let's iterate (no pun intended) on some of the examples given in the RFC, where postfix Async context with long method chainsIn the following example it is argued that postfix Combinators with closures don't allow this, therefore we need postfix I can perfectly write the example that is given in the Async section with existing prefix match context.client
.post("https://example.com/crabs")
.body("favourite crab?")
.send()
.await {
Err(_) => Ok("Ferris"),
Ok(resp) => resp.json::<String>().await,
} // I can even chain further after the match block, no problem at all
.map(...); Here is another equivalent (async) example (with longer method chains at the end and short circuit let x = match Foo.bar().baz().await {
Ok(Baz) => Some(Baz.await),
_ => None,
}
.ok_or("Failed".to_string())?
.map(|res| format!("{:#?}", res))?; Be honest - have you looked at it and immediately thought: "Nawww, I wish they'd used postfix (Method) chains vs. procedural blocks (or: keywords set expectations on control flow)Keywords set certain expectations about the control flow that is going to be introduced. When I see (method) chains being used, I can be sure that there is mostly a linear control flow top to bottom. I only have to scan for the short-circuiting operator When using postfix When using prefix For me, this is the most profound difference in prefix Conclusion by looking at the Rust survey resultsAs others have noted, the Rust survey results' conclusion has been:
Note, that there is not a single mention of pattern matching that people are struggling with (which this RFC promises to simplify). My conclusion is that this RFC goes completely against the results of the survey, the actual needs of Rust users and should therefore not be prioritized. I appreciate the authors time and investment in formulating this RFC, though. Without constantly questioning the status quo, we wouldn't have the excellent postfix |
Yes, I can understand the order of execution more easily with postfix match. The verbs in the code should be read in the order of "post body send await match", not "match post body send await". To me this is an advantage that postfix match provides (I'm aware you provided counter arguments later in "Keywords set certain expectations", I just though I'd answer your question). |
@truppelito Yes, I can see your argument and reasoning now - thank you for the quick reply. Even if we only look at it from that perspective (and ignore other arguments), I just don't think this is impactful enough to require a whole new language feature. |
If I could go back in time, and change it to be infix, I would. But I don't think it is worth supporting it as both a prefix and infix keyword. I'd rather have support for postfix macros, and a implement postfix match as a macro, probably in a community crate, at least at first. |
Having two versions of the same syntax does open up for having prefix |
Personally, I would expect the other way: for people to use postfix match more and more as they get used to it, until prefix matching is just a legacy option. |
I can give a data point that I wouldn't follow that curve... but then I choose not to use languages like Haskell because I find that going too expression-oriented makes code hard to read and maintain. |
even if you choose to bind some value to a name, there's basically no intrinsic benefit of let x = some(long, call, here);
// completely readable if the language did this in 1.0
x.match {
Ok(a) => println!("{a}");,
Err(e) => eprintpn!("{e}),
} |
I think we'll have to agree to disagree on that. I see it as mildly more work to parse, even in that simplified "not enough value to justify engaging in disruptive change" example. Not as bad as languages that use keywords instead of punctuation for delimiting blocks, but still worse. (Same for currying-centric languages where you don't need parens to make function calls.) Even if for no other reason, I don't want to see period-delimited postfix non-method special cases proliferate without a degree of justification I'm not yet convinced exists here. |
this is exactly how i feel about this rfc, and why i think having this while also discussing postfix macros makes this pointless. this would be an utterly trivial macro to make if you really wanted it, and it would turn the first class support of this one specific keyword into a useless c++-itus remnant. in my honestly blunt opinion, this is a pointless feature that adds weird specific complexities and will be shadowed by a far more useful feature being developed in parallel |
Honestly, I'd agree with this. If I had any confidence at all that postfix macros will ever exist. Which... will they? |
On the topic of postfix macros, it's worth mentioning that this very same point was brought up during the discussions for The feature made it in anyway, in no small part thanks to the IDE support it can boast. Sure, |
This comment was marked as duplicate.
This comment was marked as duplicate.
Frequently requested changes | Fundamental changes to Rust's syntax (emphasis mine)
Has this been considered for the syntax change in this RFC?
Thank you ❤️ |
|
||
x.match { | ||
Some("") | None => "Ferris" | ||
x @ Some(_) => &x[1..] |
There was a problem hiding this comment.
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).
Experimental feature postfix match This has a basic experimental implementation for the RFC postfix match (rust-lang/rfcs#3295, #121618). [Liaison is](https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/topic/Postfix.20Match.20Liaison/near/423301844) ```@scottmcm``` with the lang team's [experimental feature gate process](https://github.com/rust-lang/lang-team/blob/master/src/how_to/experiment.md). This feature has had an RFC for a while, and there has been discussion on it for a while. It would probably be valuable to see it out in the field rather than continue discussing it. This feature also allows to see how popular postfix expressions like this are for the postfix macros RFC, as those will take more time to implement. It is entirely implemented in the parser, so it should be relatively easy to remove if needed. This PR is split in to 5 commits to ease review. 1. The implementation of the feature & gating. 2. Add a MatchKind field, fix uses, fix pretty. 3. Basic rustfmt impl, as rustfmt crashes upon seeing this syntax without a fix. 4. Add new MatchSource to HIR for Clippy & other HIR consumers
Since I bors'd the PR, I wanted to respond here. One of the things that I at least am trying to do (and I think lang as a whole) is to not be a blocker unnecessarily for easily-revocable decisions. That means that even if some members are at "probably not, but I guess it's not impossible that I could be convinced", that's not enough to say that things can't land as experiments on nightly -- subject, of course, to T-compiler oversight about whether the maintenance burden is worth it given the extra uncertainty. As I said when I signed up to liason it,
Since it turned out to not be a big deal in the compiler code and it didn't trip any of my "clearly no way" flags -- for example, You should think of that as having less weight than even an initial disposition on an RFC, and anyone with new arguments for or against doing this should absolutely post them here. Hopefully having it in nightly makes it easier for both sides to demonstrate their posts more concretely, since one can make runnable examples. |
I wanted to address this point specifically because I agree with that sentence, but not the later conclusion. To explore this via a short diversion to a different feature from the one in this RFC: Why was it that, despite a bunch of people who really wanted I think it's exactly what you said: That's not true, however, of So jumping back to this RFC, which is That's why I consider That's not to say we should do it, necessarily. The TMTOWTDI counter-arguments are still strong. (But they're also not nearly as strong as for |
Thank you, @scottmcm, for referring to this argument specifically. ❤️
I totally agree with all of this! Thank you for dissecting it so clearly! However, this is not my main argument. I'm not interested in what happens before the
To illustrate these points further, here are some more examples, so that we can have a basis for further discussions: Example 1 - Postfix match - function chain that isn't anyfn do_something_special(quux: &mut Quux) {
foo
.bar
.map(|x| x.to_baz())
.filter(|x| x.is_baz())
.map(|x| x.whatever())
.match {
Some(Baz(n)) => quux.fill(n),
_ => {}
}
} This example looks like we chain multiple values together in order to return something from the function... (can easily be assumed, because of missing The only other construct in Rust today (that I can think of) that looks very similar to the above example is Example 2 - Return from a function chain within
|
The |
@kennytm The important bit in my argument is
|
Argument expressions in your chain can contain control flow too, like |
This is true - good catch! 👍 One could also introduce a block (where a value is expected), supporting your argument, like: foo
.bar({
if !cond {
break;
}
my_val
}) But if I'd encounter such code (which is rare!), I'd think about restructuring it for better readability (with "such code" I mean using With postfix |
What about any other function with a return type of unit? The match could be factored out into a separate method with a return type of unit, and it wouldn't be any more apparent that the expression doesn't return a value. |
Yes, this is true, good point! 👍 But if "factoring out match part into own function" was a common theme in regular code, we wouldn't need postfix |
Experimental feature postfix match This has a basic experimental implementation for the RFC postfix match (rust-lang/rfcs#3295, #121618). [Liaison is](https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/topic/Postfix.20Match.20Liaison/near/423301844) ```@scottmcm``` with the lang team's [experimental feature gate process](https://github.com/rust-lang/lang-team/blob/master/src/how_to/experiment.md). This feature has had an RFC for a while, and there has been discussion on it for a while. It would probably be valuable to see it out in the field rather than continue discussing it. This feature also allows to see how popular postfix expressions like this are for the postfix macros RFC, as those will take more time to implement. It is entirely implemented in the parser, so it should be relatively easy to remove if needed. This PR is split in to 5 commits to ease review. 1. The implementation of the feature & gating. 2. Add a MatchKind field, fix uses, fix pretty. 3. Basic rustfmt impl, as rustfmt crashes upon seeing this syntax without a fix. 4. Add new MatchSource to HIR for Clippy & other HIR consumers
I think currently it is most common to use a temporary variable, because using a match in a chain is currently pretty awkward, since it is a prefix keyword. |
Postfix match makes a ton of sense to me and I would love to have it in the language. It's essentially a universal I think if you see As previously mentioned, it could make redundant, or at least provide alternative to, many different combinators/transformers on Option/Result and in async chains. The bar for writing "weird" code with it is perhaps lower (is it?), but as pointed out, you can already do this in a handful places by mis-using block expressions, and I think it would be pretty straightforward to add a "control-flow-in-postfix-match" clippy lint, and very easy to spot in code review. I don't think "people might write weird code with it" is a compelling argument against an otherwise genuinely useful potential language feature, at least to me. This is the most convoluted if loop {
break {
let x = 0;
(x != 0).then(match break x == 0 { _ => || unreachable!() }).is_some()
}
} {
println!("teehee");
} |
Rendered
An alternative postfix syntax for match expressions that allows for interspersing match statements with function chains
as syntax sugar for