Open joshtriplett opened 4 months ago
Nominating because this is making a proposal for the 2024 edition.
I see there is no mention of pattern types though it seems they would be similar but distinct use of is
as an operator?
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?
so, does v as i32 is 5
parse as (v as i32) is 5
or v as (i32 is 5)
? or is it ambiguous and errors, requiring parenthesis?
@fbstj wrote:
I see there is no mention of pattern types though it seems they would be similar but distinct use of
is
as an operator?is this a pre-requisite of pattern types (to get the keyword in the language?) or does it conflict with the types usage?
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:
when combined with pattern types, what way does the precedence go? so, does
v as i32 is 5
parse as(v as i32) is 5
orv as (i32 is 5)
? or is it ambiguous and errors, requiring parenthesis?
I've added some text to the RFC, stating that this should require parentheses (assuming pattern types work with as
).
What patterns does is
enable that aren't covererd by matches!
?
@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 is
expressions and let
chains. They serve exactly the same purpose, the only difference being their reading order. I can understand the argument that we would want to have let
chains due to people expecting them to work given we already have if let
and the like but this feels like the wrong way to address that. I would instead expect us to deprecate if let
and while let
in favor of is
and dropping let
chains.
I feel like that should be added to the alternatives and/or pad out the feature duplication drawbacks paragraph.
@Veykril wrote:
I would instead expect us to deprecate
if let
andwhile let
in favor ofis
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.
Adding multiple ways to do the same thing also makes teaching Rust harder: let
in Rust is everywhere: if let
, while let
, let
-chains, let ... else
, ... So you have to teach pattern matching with let
anyway. Meaning, this "right-to-left" reading order will become natural to Rust users quick. By introducing a different way, while easy and intuitive to understand, won't help much in code clarity IMO, as people are already used to reading let
patterns.
I'd epxect is
to be a pretty common variable name, so maybe worth exploring less common words, like Some(y) binds x && y > 5
or x matches Some(y) && y > 5
.
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. matches
maybe works both ways.
Yes both let Some(y) = x && y > 5
and let .. else
become extremely confusing, but humans could parse some sensibly bracketed flavors, like { let super Some(x) = foo } && y > 5
ala https://blog.m-ou.se/super-let/
If we add is
as a keyword, we should also reserve isnot
as a keyword for future NOT-patterns
if expr isnot Some(x) {
println!("error");
}
Edited: I'm sorry for some impoliteness with "must"
Author mentioned just one alternative name for is
: ~
.
But I think we should add another alternative names in RFC, like equal
or identic
:
if expr identic Some(x) && x > 3 {
println!("value = {x}");
}
@VitWW I don't think so, Vit.
@VitWW
If we add
is
as a keyword, we must also reserveisnot
as a keyword for future NOT-patterns
You speak in a commanding way ("we must"), without justification. So, having considered my own thoughts: ...I disagree! Please offer a justification for your reasoning, and especially, why it should be addressed now, not "we do this for future expansion opportunities". It seems it will simply run up against all the concerns we're already facing, and we can wait until then.
Author mentioned just one alternative name for
is
:~
. But I think we should add another alternative names in RFC, likeequal
oridentic
:
To say we should do something is better than to command, but I don't think you have explained the prior art or other reasoning why it must be addressed in the RFC. Perhaps you were building off the point that burdges made? But unfortunately equal
is also a common function name in Rust, used in e.g. polars (as public API) and the stdlib (as private), and also seems to be a reasonably popular variable name. So it at least doesn't feel obvious as to why we would go with that.
For everyone else suggesting alternative keywords, I do really recommend everyone at least check using grep.app or something similar if their recommendation is in Rust public API somewhere, and how many cases, and be forthcoming on how many examples they find. You will likely pull hundreds of pages, so you may wish to do extrapolation or more exact queries using other tools after downloading the crates.io index.
Of course, we do have our system of keyword reservation, the k#
and r#
stropping, and edition-sensitive keyword parsing, so I think this is not the only thing to consider, and we can in fact simply pick the nicest-looking syntax if it doesn't seem an overwhelming problem. But it is best if we keep in mind any induced complexity in the lexer and parser, and the community reaction, while we rummage through our collection of Pantone chips for this shed.
@joshtriplett While I think is
, er, is a fine choice, I wish to (gently) refute ~
as lacking a history as a "pattern-matching operator", and provide some background that might be worth at least reviewing. First, SQL does have bare ~
but I think it is reasonable to mostly omit considering SQL's language features, as it is deliberately unlike most other PLs for reasons beyond this discussion. However, ~=
and =~
do have prominent histories as a pattern-matching operator!
~=
as a pattern-matching operator, and even uses it as part of case
evaluation: https://developer.apple.com/documentation/swift/range/~=(_:_:)=~
, for a regex-centric pattern-matching: https://ruby-doc.org/core-2.6.3/Regexp.html#class-Regexp-label-3D~+and+Regexp-23matchObviously, the regexp-centric examples don't exactly match to the Rust pattern language, but it's clearly a popular choice if three exceptionally common procedural PLs use it. Other examples like Vimscript and PromQL also use them, but obviously that gets increasingly niche. Wiktionary even asserts ~=
is used in mathematics... but also mentions~=
is also used as an equivalent to Rust's !=
, e.g. Lua and MATLAB.
It seems to me when ~
is included in an operator's symbol, either it means that negation, or it does imply something akin to saying "roughly like...", an approximate match, which may be why Dart uses ~/
for divide-to-integer (as opposed to dividing to a double, which more accurately represents the result of 3 / 2
). Of course, that very page I just cited also mentions Dart has is
, so I only consider this to be interesting context!
Some reactions I had while walking and thinking on this earlier:
is
for legibility and I think it will probably read nicer than let chains in almost all casesis
operator as object identity which is almost only used for x is None
, which the operator here would support. A possible addition to the prior art.if let
and also let - else
, and the distinction here is currently proposed to just be a style choice.Especially as the recent language survey seemed to highlight language bloat as one of the largest risks to the language, having this purely be stylistic seems to be in direct opposition to the data.
If we were to move forward with this I'd hope that this RFC takes a stronger stance on when to use let
forms and when to use is
forms, and strongly considers the deprecation alternative.
Possible observation: by allowing expr is PAT && condition
here, users may be more likely to try PAT && condition
as match arms instead of the current PAT if condition
. We may want to allow that:
match color {
(RGB(r, g, b) | RGBA(r, g, b, _)) && r == b && g < 1 => /* ... */,
^^ - this is currently a compile error, should be `if`
_ => /* ... */
}
... I think it'd ease refactoring and papercuts when converting code between x is PAT && y { ... }
to match x { PAT && y => ... }
While I think
is
, er, is a fine choice, I wish to (gently) refute~
as lacking a history as a "pattern-matching operator", and provide some background that might be worth at least reviewing. First, SQL does have bare~
but I think it is reasonable to mostly omit considering SQL's language features, as it is deliberately unlike most other PLs for reasons beyond this discussion. However,~=
and=~
do have prominent histories as a pattern-matching operator!* Swift does use `~=` as a pattern-matching operator, and even uses it as part of `case` evaluation: https://developer.apple.com/documentation/swift/range/~=(_:_:) * Ruby offers the inverse, `=~`, for a regex-centric pattern-matching: https://ruby-doc.org/core-2.6.3/Regexp.html#class-Regexp-label-3D~+and+Regexp-23match * And part of why it does so is because Bash does it: https://www.gnu.org/software/bash/manual/html_node/Conditional-Constructs.html#index-_005b_005b
Obviously, the regexp-centric examples don't exactly match to the Rust pattern language, but it's clearly a popular choice if three exceptionally common procedural PLs use it. Other examples like Vimscript and PromQL also use them, but obviously that gets increasingly niche. Wiktionary even asserts
~=
is used in mathematics... but also mentions~=
is also used as an equivalent to Rust's!=
, e.g. Lua and MATLAB.It seems to me when
~
is included in an operator's symbol, either it means that negation, or it does imply something akin to saying "roughly like...", an approximate match, which may be why Dart uses~/
for divide-to-integer (as opposed to dividing to a double, which more accurately represents the result of3 / 2
). Of course, that very page I just cited also mentions Dart hasis
, so I only consider this to be interesting context!
Just to follow up on this a bit, particularly from a mathematical perspective. Yes, you're right that ~ has some similarity to ≈, which means "approximately equal to," and thus it makes sense as a pattern-matching operator.
However, ~=
and =~
, from a programming perspective, are far too loaded to really work well as that kind of operator. Like, I've been writing a lot of Lua lately and ~=
is just straight-up !=
in Lua.
Plus, with the way Rust tends to organise its operators, the existence of ~=
implies that there should be a standalone ~
, which wouldn't be the case here. So, I would advocate against that regardless.
Drawing to the bigger point of what this operator should be: I genuinely don't think that there's something better than is
. It's two characters, which is as long as many existing operators. People say that it's a common variable name, but I think that it's only common as a pluralisation of i
, where i_s
could easily serve that purpose. And the only other reasonable alternative that I can think of is ~
, which is shorter and less clear. Any other keywords are going to be longer, more awkward, and more likely to cause name conflicts.
I point out some of the alternatives in the RFC because I think that we should definitely include the best arguments in favour of is
in the RFC, but I genuinely do think that it's the best choice.
If we add
is
as a keyword, we must also reserveisnot
as a keyword for future NOT-patterns
I disagree, IMO not patterns can be written as !Some(_)
(!
-patterns can be used everywhere a fallible pattern is (match
, if let
, is
, let ... else
), so isn't an is!
operator).
This means there's two ways to write it, with the not operator:
!(a is Some(v)) || v == 0
or with a not pattern:
a is !Some(v) || v == 0
or a is (!Some(_) | Some(0))
You speak in a commanding way ("we must"), without justification. .... Please offer a justification for your reasoning, and especially, why it should be addressed now
@workingjubilee I'm sorry for some impoliteness with "must".
Not-patterns wasn't added also because in today rust syntax it is ugly to write them: NOT(Some(x)) = expr
and it becomes almost pretty with isnot
keyword.
Now it should be reserved a a keyword, because it is dual to is
, just like >=
/<=
; ==
/!=
and it is strange to add just one from dual pair.
But unfortunately
equal
is also a common function name in Rust
Uups
People say that it's a common variable name, but I think that it's only common as a pluralisation of
i
, wherei_s
could easily serve that purpose.
I won’t claim it’s common, but it’s probably worth noting that is
is the country code for Iceland, and so is a natural variable name for strings containing Icelandic-language text.
I prototyped this feature back in 2018 and converted rustfmt to this style, but later dropped the corresponding rustfmt branch, accidentally and unfortunately. But the experience report is preserved at least - https://github.com/rust-lang/rfcs/pull/2260#issuecomment-367158854.
I still think this is the right thing to do, and something that should have been added instead of if-let chains from the start.
It would be unfortunate if the scenario I predicted in https://github.com/rust-lang/rfcs/pull/2497#issuecomment-404860099 plays out and EXPR is PAT
is not added for social reasons because if-let chains already exist.
@petrochenkov Agreed. I think let
chains have value because if
-let
already exists and people expect let
chains to work, but I don't think that should prevent us from adding is
. That would feel like a suboptimal path caused by path dependence.
Considering the multiple bugs around temporaries that was found with let chains, perhaps we should just reserve the is
keyword in edition 2024 and give the implementation more time to mature?
Just because I haven't seen anyone comment on it yet, I would like to know if my intuition that is
should have higher precedence than ==
(but still recommend parentheses, similar to mixing &&
and ||
) matches others' intuition as well. I could just be an outlier here and would love if others pitched in how they feel as well.
Particularly this thread: https://github.com/rust-lang/rfcs/pull/3573#discussion_r1492740859
Feel free to just thumbs up/thumbs down to express support if you don't have much else to add.
@clarfonthey wrote:
Just because I haven't seen anyone comment on it yet, I would like to know if my intuition that
is
should have higher precedence than==
(but still recommend parentheses, similar to mixing&&
and||
) matches others' intuition as well. I could just be an outlier here and would love if others pitched in how they feel as well.
My intuition tells me "there is no possible circumstance in which I would ever want to see these combined without parentheses", which makes me feel that it's irrelevant what their relative precedence is.
(I think that's true for a few other cases in the existing precedence table as well.)
That is disappointing to hear. People tend to eschew parentheses where they are unnecessary because the language already has many cases where some kind of parenthetical or brace or bracket is already either mandated by the syntactic form or is mandated by expressing the desired result, and it does not actually make the code significantly less clear to imitate Lisp slightly less.
why would any need either boolean == (x is Some(z))
or (value == y) is true
so frequently that one or two pairs of parenthesis are going to bother them :confused:
People tend to eschew parentheses where they are unnecessary
That's my preference as well, for cases that are widely parsed correctly by people who don't have the precedence table memorized. But for instance, the lint against using && and || together without parentheses is a good example where we suggest that they are more necessary than the precedence table would otherwise indicate. I think there are some cases that are intuitively obvious to people, and others where if you don't have the precedence table memorized you're likely to find them confusing. And I've regularly seen confusion about (for instance) the parsing of as
.
I do personally think mixing == and is
without parentheses seems more likely to lead to confusion than clarity. If many people feel strongly in the other direction, I could imagine changing that from "parentheses are always required" to "warning lint for not using parentheses", like && and ||. In any case, I will include it in the alternatives section.
On parens:
The safe thing to do is start out always requiring them, since then we could look at how the code comes out with them, and remove the requirement as a non-breaking change later once we have evidence.
(NOT A CONTRIBUTION)
I feel confused by this proposal in general, I just don't ever experience the problem it alleges to be solving (and I never want let chaining either, for that matter). I don't like the idea of introducing more syntax and reserving more keywords just for the purpose of reducing nested blocks or making code read from left to right. This seems to me like it increases cognitive load on all users, especially new users, to advantage a certain coding style.
I also feel surprised by this proposal appearing now for 2024. It feels like very short notice for adding a new operator in the 2024 edition, but maybe I just haven't been following the relevant conversations closely enough and to many other people this idea is common knowledge.
The safe thing to do is start out always requiring them, since then we could look at how the code comes out with them, and remove the requirement as a non-breaking change later once we have evidence.
Please especially don't do this. You already decided to do something like this once with trait bounds and Fn
traits and the weird arbitrary paren errors lingered for years as a result of how that was implemented. Please figure out the correct precedence before you stabilize the feature.
I just don't ever experience the problem it alleges
@withoutboats Take a look at the Clippy code base. Writing a lint is in large parts writing let
-chains. You really don't want to nest all the if
-expressions or early return for all of them. We've been using the if_chain
crate for years, until let
-chains were stable-enough (they are still not stabilized).
I still think this is the right thing to do, and something that should have been added instead of if-let chains from the start.
As let
-chains are not stabilized yet, and iff there is consensus that the is
approach is better, I think we should go with the is
approach and remove let
-chains again. I just think having both can cause problems and confusion, as I argued above.
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.
I agree that having syntax for the two is confusing and we should choose only one because both are semantically the same. We should choose the one that is more straightforward for the users.
Users are already used to the "backwardness" of if let
so while I personally like is
a bit more I prefer let chains because they are easier to learn[citation needed] and focus the effort on stabilizing those.
Right now, "Reference-level explanation" does not tell us how to use "IsExpression
", except
"Detect is appearing as a top-level statement and produce an error, with a rustfix suggestion to use let instead" (but in "Future possibilities" and comments we see much more limitations).
So I write rules more explicitly:
IsExpression :
Expression is PatternNoTopAlt
BoolExpression :
LazyAndExpr | IsExpression | Expression
LazyAndExpr :
BoolExpression && BoolExpression
PredicateLoopExpression :
while BoolExpression /*except struct expression*/ BlockExpression
IfExpression :
if BoolExpression /*except struct expression*/ BlockExpression
(else ( BlockExpression | IfExpression ) )?
These rules could also be extended to match
MatchArmGuard :
if BoolExpression
These rules could be unified together with let-chains
if we just rewrite BoolExpression
a bit:
LetExpr :
let Pattern = Scrutinee
BoolExpression :
LazyAndExpr | LetExpr | IsExpression | Expression
For the binding question I think we only need answer if the following are going to be well-defined or not. I think it covered all kinds of expressions including the unstable ones.
(preferred: binding won't escape the block, all of below are ill-formed because all w > 0
expression will cause E0425 "cannot find value `w`" error.)
// 01. Block expression
{ x is Some(w) } && w > 0;
// 02. Break
('a: {
if cond {
break 'a val1 is Some(w);
}
val2 is Some(w)
}) && w > 0;
// 03. Unsafe block expression
unsafe { x is Some(w) } && w > 0;
// 04. Const block expression
const { X is Some(w) } && w > 0;
// 05. If expression
(if cond {
val1 is Some(w)
} else {
val2 is Some(w)
}) && w > 0;
// 06. Match expression
(match val {
Ok(a) => a is Some(w),
Err(b) => b is Some(w),
}) && w > 0;
// 07. Inline const pattern
#![feature(inline_const_pat)]
match true {
const { Some(1) is Some(w) } if w > 0 => { w },
_ => unreachable!(),
}
||
expressions(11 must be well-formed, while 12 and 13 are preferred to be well-formed too)
// 11. Distinct variables but unused
if val1 is Some(x) || val2 is Some(y) {
// not using x and y here.
println!("good");
}
// 12. Same variables and used
if val3 is Some(w) || val4 is Some(w) {
println!("w = {w}");
}
// 13. Overlapping variable set
if val5 is Some((a, b)) || val6 is Some((b, c)) {
println!("b = {b}");
}
(preferred: well-formed, seems harmless)
// 21. Cast
(x is Some(w)) as bool && w > 0;
After thinking about this for some time, my conclusions are:
is
syntax is superior to let
-chains, because it works in more context.is
can be used in more situations than let
-chains, and can do everything that let
-chains can, we should not have let
-chains.
is
should always be preferred over let
-chains, see the next item.is
can interact reasonably well with the existing if let
and while let
:
if let
as a "conditional let
", i.e. the focus is on introducing a new binding.is
, the focus is on matching a pattern.&&
, you're doing more pattern matching than binding creation, so is
would be more appropriate.if let ... = ... {}
with no binding on the LHS should be is
, and if ... is ... {}
with a binding on the RHS should be if let
).is
introduces bindings into the body of a conditional should be considered a nice side-effect, not its primary purpose.Btw, I think it would be good for the RFC to be explicit about the following (i.e., include a snippet show it working):
is
in the condition of a while
loop (and the fact that any bindings can be used inside the loop).is
inside a match
guard (and whether or not any bindings from the guard can be used inside the match
arm body).One difference exists between let-chains
and is-expressions
: operator precedence
lack of
=
(or/andlet
) in the example confuses them
not to say their experience is invalid, but a match
arm and a for
loop also created bindings without =
or let
.
Like, I've been writing a lot of Lua lately and ~= is just straight-up != in Lua.
Likely because, in C, ~
means bitwise complement and various languages (Rust included) have merged logical and bitwise NOT into a single type-switched operator.
why would any need either
boolean == (x is Some(z))
or(value == y) is true
so frequently that one or two pairs of parenthesis are going to bother them 😕
I don't really see the use in requiring people to write stuff like
iter.filter(|(i, item)| (i % 2 == 0) == (item is Some((a, b))))
because we're afraid of defining a precedence.
@workingjubilee
You realize you already can't write x == y == z
without parenthesis? (same for other comparison operators, <
, <=
, >
, >=
, !=
)
error: comparison operators cannot be chained
--> src/main.rs:2:18
|
2 | let a = true == true == true;
| ^^ ^^
|
(NOT A CONTRIBUTION)
not to say their experience is invalid, but a match arm and a for loop also created bindings without = or let.
There's a connection between this user feedback and the binding question.
let
is the only construct which creates bindings in subsequent sibling nodes of the AST, these other bindings are only in scope in child nodes of the AST. (This is also a difference of Rust's let
from the let
in ML, a difference that has precedent in a lot of imperative languages and ultimately derives from the original 50s-era imperative languages without block structure at all).
I think this user is expressing an intuition that this distinguishes let
bindings from other bindings; recognizing this as a property of let
would push toward the let chaining rather than new syntax that doesn't involve let
.
This also can give guidance about the binding question: surely the binding rules for this or let chaining should never allow you to bind something in a parent node, which for example the block structures and type casts would all do.
As a passerby, if I could recreate Rust from scratch, I'd definitely go with $expr is $pat
over if let
/while let
. As a matter of fact, I've already implemented a similar syntax in a hobby language. However, as it stands, it seems like it's much too late to make this change. If the introduction of is
expressions leads to the deprecation of {if,while} let
expressions in Rust 2024+ (and I don't see why it wouldn't[^1]), it would cause a disproportionate amount of churn for a tiny increase in flexibility and learnability (and, if memory serves, my struggle in learning if let
was less "I don't understand this" and more just "why is it backwards?").
I believe the Language Design Team's put it better than I ever could [^2]:
The established Rust community with knowledge of existing Rust syntax has a great deal of value, and to be considered, a syntax change proposal would have to be not just better, but so wildly better as to overcome the massive downside of switching.
It isn't clear to me that the ability to use binding patterns in more places is wildly better than relying on/extending Rust's existing pattern matching constructs. As far as I can tell, let
chains would give 80% of the power of is
expressions while causing significantly less churn (largely from collapsing if let _ = _ { if _ {} }
and migrating if_chain!
invocations).
[^1]: The RFC motions against this, but something about having both {if,while} let
expressions and is
expressions feels out of place with respect to Rust's design principles. Even if this RFC is merged as-is, I could easily see people pushing for the deprecation of {if,while} let
in Rust 2027.
[^2]: I know the author of this RFC is part of the lang team and likely kept this principle in mind as he wrote it; I'm just using this to explain my point.
I think this user is expressing an intuition that this distinguishes let bindings from other bindings; recognizing this as a property of let would push toward the let chaining rather than new syntax that doesn't involve let.
So I suppose if the construct is spelled $expr is let $pat
(or $expr = let $pat
? :upside_down_face:) it would be easier to teach that a binding is introduced.
if expr_producing_option() is let Some(v) && condition(v) { use(v); }
if color is let (RGB(r, g, b) | RGBA(r, g, b, _)) && r == b && g < 10 {
println!("condition met")
}
func(x is let Some(y) && y > 3);
OTOH, the is let
here is only constrained within the current expression statement and not its siblings, which might be confusing from the other direction (though if let
/ while let
's binding also terminate within its own block).
@joshtriplett
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.
Could be worth mentioning that there would be natural interaction between x is PAT
and pattern types, if x
would/could be flow typed to T is PAT
in the scope where the test evaluates to true. So there would be a pleasant syntactic parallel if is
can be used for both.
(NOT A CONTRIBUTION)
So I suppose if the construct is spelled $expr is let $pat (or $expr = let $pat? 🙃) it would be easier to teach that a binding is introduced.
I think if the community really decides that the pat/expr ordering of let
bindings is a "problem" worth solving with more syntax, the solution that comes to mind is to allow all let bindings to be written something like let $expr is $pat
. I think this is a bad idea (for the same reason I think #3295 is a bad idea) and you should just go with let
chaining without changing the order on the basis of this motivation, but that would at least be consistent with Rust's binding rules and not introduce questions about binding in ||
patterns or into the parent scope like you previously identified.
OTOH, the is let here is only constrained within the current expression statement and not its siblings, which might be confusing from the other direction (though if let / while let's binding also terminate within its own block).
The way to parse this is that the siblings are the other conditionals and the parent is the if
, not the block the if
is in. People who expect let
chaining to work seem to be operating off the same intuition. Of course this runs into weirdness with the ||
joining, so you can only &&
them, revealing the way in which this thought process is sort of fuzzy (but if people have enough problem with deeply nesting their if let
s then I guess let chaining is the answer to that problem.)
I feel like the is
operator could have its own purpose unique from if-let
and while-let
expressions, just to do with the scope of the binding. For a start, the variable binding defined by an is
expression isn't super explicit since it appears deeper into the expression, especially if it's deeply nested in parentheses; whereas if let
makes it super obvious that a variable is being defined.
Essentially, this example wouldn't compile and might instead tell you to use an if-let
binding if you want to use the variable inside the block:
if an_option is Some(x) && x > 3 {
println!("{x}");
}
Maybe parentheses (and certain other operators like ||
?) would define the scope of the binding within the condition, so you could reuse the same variable multiple times without shadowing.
if (an_option is Some(x) && x > 3) || (another_option is Some(x) && x < 3) {
println!("awesome, I just can't use x in here!");
}
If the point of this is to improve readability, I think this would serve the job better.
Although, one thing that could be confusing is shadowing an existing binding with the is
expression. I guess the block would be able to refer to the original binding while ignoring the one in the condition, which might seem ambiguous/confusing?
Also, at this point the is
expression would basically just be syntax sugar for the matches!
macro or Option::is_some_and
style methods, which I don't think is necessarily a bad thing (besides the possibly confusing shadowing, which you don't really get with the macro or method).
@workingjubilee
You realize you already can't write
x == y == z
without parenthesis? (same for other comparison operators,<
,<=
,>
,>=
,!=
)error: comparison operators cannot be chained --> src/main.rs:2:18 | 2 | let a = true == true == true; | ^^ ^^ |
When I chose my example, I spent a while carefully choosing an expression someone might actually write, and that closely resembles code I have written before, that I also feel illustrates the problem reasonably well. I have a problem tracking all the parentheses, you see, and I often carve apart expressions when moving around code using the wrong pair of braces or parens. It leads to me often rewriting code from scratch instead of the simpler operation of cut and paste. It's a problem with how I read the code, and it leads to bigger, more "explicit" expressions being even worse, because it gets harder for me to find the splitting points.
So who, exactly, cares about let a = true == true == true;
? I would like to meet them.
@workingjubilee
So who, exactly, cares about
let a = true == true == true;
? I would like to meet them.
You wrote iter.filter(|(i, item)| (i % 2 == 0) == (item is Some((a, b))))
which is is already impossible today without parenthesis even without considering the is
part
error: comparison operators cannot be chained
--> src/main.rs:2:35
|
2 | iter.filter(|(i, item)| i % 2 == 0 == item.is_some())
| ^^ ^^
|
Maybe parentheses (and certain other operators like
||
?) would define the scope of the binding within the condition, so you could reuse the same variable multiple times without shadowing.
I would argue against the idea of "artificial" limitations, and instead argue towards greater scope, if possible.
In your very example, I would argue that x
should be usable:
if (an_option is Some(x) && x > 3) || (another_option is Some(x) && x < 3) {
println!("Here's the {x} I got");
}
In a pattern it's possible to bind x
in multiple alternative patterns (Either(x) | Or(x)
) and this regularly allows to simplify code: the part using the x
downstream need not be repeated!
In a sense, is
offers this on steroids: you can know unify patterns & additional arbitrary conditions.
Although, one thing that could be confusing is shadowing an existing binding with the
is
expression. I guess the block would be able to refer to the original binding while ignoring the one in the condition, which might seem ambiguous/confusing?
Shadowing wouldn't be a problem, as long as it's not possible to refer to the outer x
once an x
has been defined in a "header" scope: condition of if
or while
, maybe a few other situations?
I would expect a deny-by-default lint forbidding the use of the outer x
in such a situation would prevent any ambiguity.
Introduce an
is
operator in Rust 2024, to test if an expression matches a pattern and bind the variables in the pattern. This is in addition tolet
-chaining; this RFC proposes that we allow bothlet
-chaining and theis
operator.Previous discussions around
let
-chains have treated theis
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 allowlet
-chaining and add theis
operator.The
is
operator allows developers to chain multiple matching-and-binding operations and simplify what would otherwise require complex nested conditionals. Theis
operator allows writing and reading a pattern match from left-to-right, which reads more naturally in many circumstances. For instance, consider an expression likex is Some(y) && y > 5
; that boolean expression reads more naturally from left-to-right thanlet 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 left to right, andis
fits naturally at the end of such a sequence.Having an
is
operator would also help to reduce the demand for methods on types such asOption
andResult
(e.g.Option::is_some_and
andResult::is_ok_and
andResult::is_err_and
), by allowing prospective users of those methods to write a natural-looking condition usingis
instead.Rendered