-
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
is
operator for pattern-matching and binding
#3573
base: master
Are you sure you want to change the base?
Conversation
Nominating because this is making a proposal for the 2024 edition. |
is
operatoris
operator for pattern-matching and binding
I see there is no mention of pattern types though it seems they would be similar but distinct use of is this a pre-requisite of pattern types (to get the keyword in the language?) or does it conflict with the types usage? |
when combined with pattern types, what way does the precedence go? |
@fbstj wrote:
This is not related to pattern types. I believe we can do both without conflict. I added some text to the "unresolved questions" section to confirm that we can do both without conflicts. @programmerjake wrote:
I've added some text to the RFC, stating that this should require parentheses (assuming pattern types work with |
What patterns does |
@dev-ardi One example: if expr is Some(x) && x > 3 {
println!("value is {x}");
} |
I find it a bit odd that we would want both I feel like that should be added to the alternatives and/or pad out the feature duplication drawbacks paragraph. |
@Veykril wrote:
That would be a massive amount of churn for very little benefit. Nonetheless, you're right that I should add this to the alternatives section. |
Previous discussions around `let`-chains have treated the `is` operator as an | ||
alternative on the basis that they serve similar functions, rather than | ||
proposing that they can and should coexist. This RFC proposes that we allow | ||
`let`-chaining *and* add the `is` 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.
I'm a bit concerned about this. This would introduce the possibility of doing the same thing in 2 different ways on a language level. IMHO this is a bad idea, as it opens the door for mixed-style code bases, that just get harder to read.
For tooling, this is also a problem: Clippy will most likely get (restriction) lint requests for not allowing is
OR not allowing let
-chains.
Another problem I see here is: What should Clippy do when producing suggestions? If we have the policy to always suggest is
over let
-chains, that might pollute code bases where let
-chains are preferred (and vice versa). We also can't really check things like "is this a let
-chain code base" or "are we in an is
-chain expression" when producing suggestions. One lint suggesting is
and another suggesting let
will make this problem even worse, and that is almost impossible to avoid with changing contributors and team members.
We recently had the situation described above with suggesting the new-ish _ =
binding over let _ =
. We decided to suggest let _ =
as we don't have to check the MSRV before producing the suggestion that way.
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.
Rust already has many different ways to do the same thing. You can write a for
loop or you can write iterator code. You can use combinators or write a match
or write an if let
. You can write let
-else
or use a match
. You can write x > 3
or 3 < x
. You can write x + 3
or 3 + x
.
as it opens the door for mixed-style code bases, that just get harder to read
In this RFC, I'm proposing that both of them have value, and that it's entirely valid for a codebase to use both, for different purposes.
if let PAT = EXPR && ...
emphasizes the pattern and its binding. It seems appropriate for clear division into cases based primarily on the pattern, by writing if let ... else
.
if EXPR is PAT && ...
leads with the expression, then the pattern, then the next condition. It feels more appropriate for cases where you expect the reader to find it easiest to process in order of the sequence of operations from left to right: "run this EXPR, see if it matches PAT, check the next condition ..."
I personally expect to find myself writing both, in different cases.
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.
For me those are not really comparable:
- The language gives you the
for
loop, the standard library gives you the option/power to do this with iterator method chains. match
is rather if you want to match one expression to multiple variants, whileif let
is for checking if the expression is that exact variant (there's a Clippystyle
lint for this).let
-else
was introduced for a specific use case to save some lines of code over using amatch
- I hardly see how commutativity is related to this.
The proposed is
language construct doesn't do the same:
- Both
let
-chains andis
are provided as a language construct. - There's no clear rule of thumb with
is
vslet
. It's a pure style choice IMO. - It doesn't simplify (in terms of amount of code) a certain, often occurring pattern.
The second point is the biggest problem for tooling: It is impossible to determine what to suggest. With the other examples it's usually clear, because the alternative is more concise/readable/idiomatic/....
The focus on expression vs pattern I can see and think is a valid point. But to that, I want to point out the equatable_if_let
Clippy lint, that tried to address something similar, but never got out of nursery
as we (mainly I) couldn't agree when expr == pat
is preferable over pat == expr
/let pat = expr
. rust-lang/rust-clippy#7777
So I see the addition of the is
as giving the user a choice between two styles and not much more. IMO this is not worth the downsides that come with this. But that is my opinion and millage may vary obviously.
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 want to also link and quote one of my comments below: #3573 (comment)
As
let
-chains are not stabilized yet, and iff there is consensus that theis
approach is better, I think we should go with theis
approach and removelet
-chains again. I just think having both can cause problems and confusion, as I argued above.
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.
If let
-chain is to be scraped, this RFC should really have a section to refute the counterarguments made in RFC 2497.
Adding multiple ways to do the same thing also makes teaching Rust harder: |
I'd epxect I do think larger expression make the left vs right swap interesting, but remember perl created chaos with its left vs right trickery, so one should really be careful here. Yes both |
If we add if expr isnot Some(x) {
println!("error");
} Edited: I'm sorry for some impoliteness with "must" |
Add `is` to the [operator | ||
precedence](https://doc.rust-lang.org/reference/expressions.html#expression-precedence) | ||
table, at the same precedence level as `==`, and likewise non-associative | ||
(requiring parentheses). |
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 think that it should recommend parentheses, but have a higher precedence than ==
, similar to how &&
has higher precedence than ||
but we still recommend parentheses there.
To me, x is Some(z) == y is Some(w)
is unambiguous, even if I would recommend adding parentheses for clarity.
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.
There's no valid expression with ==
on both sides of an is
. But what about if a == b is false
?
If a
and b
are bool
, then the expression is ambiguous, but it returns the same result with either operator precedence. (But there might be a change in the evaluation order.)
One way to deal with expressions like this is a lint removing is true
, and turning is false
into !
.
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.
This is also discussed in the Rationale and alternatives section here:
https://github.com/rust-lang/rfcs/pull/3573/files#diff-339b745f7c3e97812ca3995c152f0cbfb7126913d78ccf09a3bad3706fb53e7aR195
instance, the standard library could additionally provide `Any::is` under a new | ||
name `is_type`. | ||
|
||
# Rationale and alternatives |
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 that I think is worth exploring here, even though I agree it's worse than the proposal, is the idea of just promoting let patterns to expressions. For example, allowing f(let Some(y) = x && y > 5)
. This is consistent with let-chaining, but noticeably uncomfortable, and worth exploring as an example of further motivation for why is
is the better option.
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 don't think it's even good for the language to promote patterns such as
f(x is Some(y) && y > 5)
That promotes very obscure code which is really hard for people new or even intermediate to the language to even understand what is going on.
I'd much rather see patterns such as
if x is Some(y) && y > 5 {
f(true);
} else {
f(false);
}
which while more verbose is less arcane. I agree that the first one looks prettier but there is a lot of information to unpack in one line, especially if you are new.
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.
Honestly, what you're describing to me is quite a stylistic choice and I don't think it's something that the language itself should have a say in, and maybe something that should be left in clippy lints.
What you've described to me is extremely similar to the common case of if x { true } else { false }
, and it generally represents some failure to fully conceptualise booleans as data, rather than just conditions on branches. This is actually extremely common among programming in general and (IMO) represents a combination of failures in teaching, misconceptions accumulated from how other languages work, etc.
Like, to be clear, this isn't me saying you're wrong here-- it's a real problem and ignoring it is not a real solution. But in that regard, while failing to dig deep into why people prefer this more expanded version is ignoring it, it's also ignoring it to just say that the expanded version is better and not question it.
This is kind of why I think that the solution probably lies somewhere in clippy-- things such as if x { true } else { false }
are warned in clippy lints, and similarly, your code would probably be changed back into mine after two passes, where the first notices that the f(...)
could be factored outside the expression to f(if ... { true } else { false })
and then the second notices that you're doing if x { true } else { false }
.
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.
If let were to be promoted to return a boolean on a successful bind it would both solve let chaining as well as the main problem with let chaining as it is proposed today. I would hate for that to be accepted instead of is
as the let
keyword feels overused enough as is. But it is my personal preference over let chaining.
from seeing `if let`/`while let` syntax. | ||
|
||
We could add this operator using punctuation instead (e.g. `~`). However, there | ||
is no "natural" operator that conveys "pattern match" to people (the way that |
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 disagree that there's no natural operator, even though I agree that it would be less clear.
For example, we could use tildes as an additional equality operator (x ~ Some(y)
) and it would fit in with Rust fine. I mostly say this because I think that the reasoning should lean heavier on its stronger arguments and not for a lack of creativity:
is
is short and immediately clear- Any use of it as a variable is something that won't be missed (e.g. as a plural of
i
, an already nameless variable) - People are already used to it being a keyword in other languages, so, it being one in Rust too isn't strange
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 to point to, is
is easy to write too, which is worth considering.
For example it annoys me to have to write #![(...)]
In my opinion we should try to avoid symbols where words suffice.
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.
For some more prior art, Raku uses ~~
for “smart matching”. Using two tildes would leave the single tilde free for something else, if we wanted to use it later. Of course, Raku is known to be a symbol-heavy language, so I don’t think this is the best choice.
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'd like to note that Rust used to use tildes (~T
), and during the move to Box<T>
it was noted that ~
is simply absent (not just hard to type, plain absent) from a number of keyboard layouts.
As an example, consider a Polish keyboard layout.
I would recommend avoiding ~
altogether.
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.
@matthieu-m The keyboard layout you've linked to is an obsolete typewriter layout. Polish computers use a QWERTY-based layout called "Polish programmer's" layout, which despite the name, is the default used by everyone.
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.
another argument that i have yet to see is that the is
keyword fits very well with the as
keyword already present in Rust
I am loving these discussions and wanted to add my 2 cents. The considerable upside for let foo = EXPR is PAT; To me this is a significantly more consistent and flexible solution as it naturally fits in with the programmers model of the code. e.g The only functional "downside" of what let Some(x) = y else { return; } which to me seems entirely superfluous as let chains however sit in direct opposition to this RFC as they aim to solve the same goal although with less flexibility. Unlike the Without let chains the I don't believe code churn is a valid argument against if the RFC doesn't propose replacing the existing operators. To the contrary it helps reduce needlessly complicated (often times requiring macros) expressions which can be solved very elegantly with Lastly I also think the keyword "is" is ideal as it complements the on a more personal note, I think that the current I also don't think asking "what does this solve that can't be solved with a macro" is fair as macros are powerful enough to write entirely new languages within rust |
While I understand the possible convenience of the I try to make it clearer why I have a hard time reading keywords like if opt_value is Some(good_var) && other_value is Some(other_var) {
// ...
} vs. if let Some(good_var) = opt_value && let Some(other_var) = other_value {
// ...
} The code using
The code using
One might notice that in the case with When I skip over code, I find it much easier to read if identifiers are separated by non-ident characters. Side note: It is also easier for parsers, because they know after the first non-ident char that it cannot become an identifier so there might be something to it. I know that languages like Pascal lean heavy into keywords instead of special characters For me skipping is much easier with languages going for short keywords and some common special characters. That is what I like so much about Rust, because it usually has good trade-offs between writing and reading code. About the argument of bad mental model using I thought, At least to me this seemed natural. |
I am unsure where this notion came from but it is entirely false. let Some(val) = exp else {
return false;
} would now be implicitly returning true if the binding succeeds. The
I think this common misconception is the reason why some people prefer let bool_value = if let Ok(Some({key: "foobar", ..})) = expr {
true
} else {
false
}; when it could so easily be: let bool_value = expr is Ok(Some({key: "foobar", ..})); I would also like to point out that making functional choices based on syntax is probably not the best idea. I have always respected Rust for (mostly) having only 1 way of doing things. For being consistent in almost any aspect and offering a great deal of flexibility due to it. The example that sticks out to me the most is the fact that everything is an expression. This means that rust never had to introduce a ternary operator since
println!("Value is valid? {}", val is Ok(Some(_))); Making arguments as to stylistic preference or readability promotes pointless arguing and bikeshedding. Everybody has an opinion on what looks or feels better. Which is why I implore you to consider from a purely functional perspective which is better. We should be discussing these proposals on their technical ability and functional merit, not based on opinion. To this end I also propose that the "readability" aspect of the RFC be dropped in favor of functionally comparing the two solutions and the possibilities that arise. If Footnotes
|
This seems to be one of the more common arguments in favor of Considering the sentiment against it as an alternative for |
it can easily be this today in stable Rust let bool_value = matches!(expr, Ok(Some(Struct{key: "foobar", ..}))); when we get postfix macro #2442 you could even use let bool_value = expr.matches!(Ok(Some(Struct{key: "foobar", ..}))); if we are to remove the binding ability of |
There is one advantage I can think of, which is that you could combine a scoped if args.is_empty()
&& &callee.kind is ExprKind::Path(qpath)
&& self.typeck_results.qpath_res(qpath, callee.hir_id) is res
&& res.opt_def_id() is Some(def_id)
&& self.lcx.get_def_path(def_id) is def_path
&& def_path.iter().take(4).map(Symbol::as_str).collect::<Vec<_>>() is def_path
&& let ["core", "num", int_impl, "max_value"] = *def_path
{ but I'd agree that a postfix |
@Yokinman As I pointed out already, Using macros as an argument against any language feature does nothing but hinder progress. Rust macros are some of the most powerful around, to the point that they let you write entirely new programming languages within rust. You are saying "hey this tool that can act as its entirely own compiler can also do the thing you are proposing" which is the case for anything. We don't really need postfix macros in that case since There is a Macro for that too. Either Rust as a language stagnates as every possible change is denied due to "there is a macro for that" or we accept that maybe adding things to the language that already exist in macro form can be a good thing. It helps reduce compile time by removing yet another dependency and stops there being 5 different versions of the same functionality. |
@CEbbinghaus That's fair... however, it must also be said that: First...
Second, the Rust devs have a history of adopting an "If it can be implemented as a macro, let the ecosystem iterate on it, and we'll see if there's enough demand for the third-party macro crates to justify incorporating it" stance. That's why we still don't have delegate as part of the language itself... and, as someone who's been here since before v1.0, I can say that there have been a lot of vocal people who have wanted some form of implementation inheritance over the years. ...and I think that middle-ground is a reasonable stategy. Heck, that's why the standard library is in the process of gaining a replacement for |
@ssokolow Fully agree with everything you said. While macros may not be a valid reason for saying "no" to a feature they are absolutely an argument for the feature. However the problem with this particular proposal is that is stands in direct opposition to another RFC that is currently being worked on. If the approach is taken to "let the community cook" in order to figure out how best to implement this and what the developers need from it, Then the proposal will never be accepted as in the mean time the There is no future for this RFC if the only argument for is "more ergonomy" as that is already highly controversial & hotly debated. If this RFC is a functionally more flexible and consistent solution compared to The RE
I would actually argue that let is_small = a.map(|x| x < 10).unwrap_or(false);
let is_small = a.is_none() || a.is_some_and(|x| x < 10);
let is_small = a is None || a is Some(x) && x < 10; The programmers mental model is strongest when there are small blocks like lego bricks that they can put together however they want. Having 1 specific method for each possible match requires memorizing significantly more and adds a lot more methods to the docs that people need to read through. While a simple concept like |
That's also fair. I've already posted my reasons for feeling it's not the best fit for Rust but, in the context of that view of it, I think my main concern is that it feels more like a "would have been great if we thought of it before v1.0, when we could still have gotten away with getting rid of As-is, it just feels like it would be too much of a subversion of the effort that's gone into making Rust's features more clean, orthogonal, and well-factored. |
can already be written as: let is_small = matches!(a, None | Some(x) if x < 10);
I agree. The pattern matching syntax in As @ssokolow points out, pre v1.0, there was perhaps a better case for I also think I very rarely use |
It is not a binary decision between "accepting every syntax change and the kitchen sink" or "reject all changes we have a is None || a is Some(x) && x < 10 also relies on the binding a temporary name (BTW this particular expression can be written simply as
most time spent on compiling is codegen, whether using
there is only a single version of
adding a new keyword introduces a very high barrier to entry. you are basically going to require these 2k lines of variables and 4k lines of functions to rename
the binding rule issue is not something you could "However" out |
At this scale, support for |
Syntax wise, I wonder about |
@nikomatsakis Your Scala is showing 😉 Though, that's actually a surprising choice to me, as I'd expect // regular postfix `match`
fn is_7(x: u8) -> bool {
x match { 7 => true, _ => false }
}
fn is_some_7(x: Option<u8>) {
if x match { Some(7) => true, _ => false ) {
println!("it was seven");
} else {
println!("it wasn't seven");
}
} I don't actually want to do this. Combining prefix |
Love me some Scala. I still want _ closures. :) |
This came up again in a libs-api meeting today. We're still getting proposals to add simple property-matching functions such as rust-lang/libs-team#357 So even with let changing the libs team is still interested in a way to have a universal language to express these rather than adding new functions and inventing new nomenclature for each new type which users then have to learn separately. |
|
That's presupposing those functions would exist. We're not going to accept every single function, a language feature would cover them. Some enums have dozens of variants and nobody will implement functions for them. And ideally it'd be combined with enum type inference, e.g. |
I'd argue that it's like the difference between struct literals and a constructor function, or the difference between direct variable access and a getter/setter function. The pattern match is more direct in the sense that you know exactly what it's doing at face value, but with a function you gotta look under the hood to be sure. |
I can't find anywhere in this (3573) RFC proposing making Inferred type #3444 for |
@kennytm wrote:
I totally agree. I propose the following solution: macro_rules! scope {
($a:expr) => { if $a { true } else { false } }
} And now you can use let x = 9999;
let y = Some(4);
if y is Some(x) && x > 0 {
println!("{x}"); // Works, 4
}
if y is Some(x) && x > 0 || cheat_code_enabled() {
println!("{x}"); // Hard error, "is" is not allowed here
}
if scope!(y is Some(x) && x > 0) || cheat_code_enabled() {
println!("{x}"); // Works, 9999
} So, now everything works, but potentially ambiguous code is rejected and Also, you can additionally allow if { y is Some(x) && x > 0 } || cheat_code_enabled() {
println!("{x}"); // Works, 9999
} |
an_expression() is x; | ||
``` | ||
|
||
# Reference-level explanation |
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.
We should cover the drop order in this RFC. My suggestion would be that Some(_) is y
should drop y
immediately and that Some(x) is y.as_ref().map(|x| x.len())
should drop the temporary immediately.
Rather than disalowing the if y is Some(x) && x > 0 || cheat_code_enabled() {
println!("{x}"); // error[E0381]: used binding `x` is possibly-uninitialized
} which you can already write as let x;
if (if let Some(temp_x) = y { x = temp_x; x > 0 } else { cheat_code_enabled() }) {
println!("{x}"); // error[E0381]: used binding `x` is possibly-uninitialized
} similar to let x;
if cond {
x = 1;
}
println!("{x}"); // error[E0381]: used binding `x` is possibly-uninitialized If you need access to the shadowed variable you can always rename one of the bindings so that they no longer shadow each other. |
Introduce an
is
operator in Rust 2024, to test if an expression matches apattern and bind the variables in the pattern. This is in addition to
let
-chaining; this RFC proposes that we allow bothlet
-chaining and theis
operator.Previous discussions around
let
-chains have treated theis
operator as analternative on the basis that they serve similar functions, rather than
proposing that they can and should coexist. This RFC proposes that we allow
let
-chaining and add theis
operator.The
is
operator allows developers to chain multiple matching-and-bindingoperations and simplify what would otherwise require complex nested
conditionals. The
is
operator allows writing and reading a pattern match fromleft-to-right, which reads more naturally in many circumstances. For instance,
consider an expression like
x is Some(y) && y > 5
; that boolean expressionreads more naturally from left-to-right than
let Some(y) = x && y > 5
.This is even more true at the end of a longer expression chain, such as
x.method()?.another_method().await? is Some(y)
. Rust method chaining and?
and
.await
all encourage writing code that reads in operation order from leftto right, and
is
fits naturally at the end of such a sequence.Having an
is
operator would also help to reduce the demand for methods ontypes such as
Option
andResult
(e.g.Option::is_some_and
andResult::is_ok_and
andResult::is_err_and
), by allowing prospective users ofthose methods to write a natural-looking condition using
is
instead.Rendered