Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

is operator for pattern-matching and binding #3573

Open
wants to merge 10 commits into
base: master
Choose a base branch
from

Conversation

joshtriplett
Copy link
Member

@joshtriplett joshtriplett commented Feb 16, 2024

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 to
let-chaining; this RFC proposes that we allow both let-chaining and the
is operator.

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.

The is operator allows developers to chain multiple matching-and-binding
operations and simplify what would otherwise require complex nested
conditionals. The is operator allows writing and reading a pattern match from
left-to-right, which reads more naturally in many circumstances. For instance,
consider an expression like x is Some(y) && y > 5; that boolean expression
reads 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 left
to 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 on
types such as Option and Result (e.g. Option::is_some_and and
Result::is_ok_and and Result::is_err_and), by allowing prospective users of
those methods to write a natural-looking condition using is instead.

Rendered

@joshtriplett joshtriplett added T-lang Relevant to the language team, which will review and decide on the RFC. A-edition-2024 Area: The 2024 edition I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. labels Feb 16, 2024
@joshtriplett
Copy link
Member Author

Nominating because this is making a proposal for the 2024 edition.

@joshtriplett joshtriplett changed the title RFC for the is operator is operator for pattern-matching and binding Feb 16, 2024
@fbstj
Copy link
Contributor

fbstj commented Feb 16, 2024

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?

@programmerjake
Copy link
Member

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?

@joshtriplett
Copy link
Member Author

@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 or v 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).

@dev-ardi
Copy link

What patterns does is enable that aren't covererd by matches!?

@joshtriplett
Copy link
Member Author

joshtriplett commented Feb 16, 2024

@dev-ardi One example:

if expr is Some(x) && x > 3 {
    println!("value is {x}");
}

@Veykril
Copy link
Member

Veykril commented Feb 16, 2024

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.

@joshtriplett
Copy link
Member Author

joshtriplett commented Feb 16, 2024

@Veykril wrote:

I would instead expect us to deprecate if let and while let in favor of is

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.
Copy link
Member

@flip1995 flip1995 Feb 16, 2024

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.

Copy link
Member Author

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.

Copy link
Member

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, while if let is for checking if the expression is that exact variant (there's a Clippy style lint for this).
  • let-else was introduced for a specific use case to save some lines of code over using a match
  • I hardly see how commutativity is related to this.

The proposed is language construct doesn't do the same:

  • Both let-chains and is are provided as a language construct.
  • There's no clear rule of thumb with is vs let. 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.

Copy link
Member

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

Copy link
Member

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.

@flip1995
Copy link
Member

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.

@burdges
Copy link

burdges commented Feb 16, 2024

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/

@VitWW
Copy link

VitWW commented Feb 16, 2024

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"

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).
Copy link
Contributor

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.

Copy link
Contributor

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

Copy link
Contributor

Choose a reason for hiding this comment

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

instance, the standard library could additionally provide `Any::is` under a new
name `is_type`.

# Rationale and alternatives
Copy link
Contributor

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.

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.

Copy link
Contributor

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

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
Copy link
Contributor

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

Copy link

@dev-ardi dev-ardi Feb 16, 2024

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.

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.

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.

Copy link
Contributor

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.

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

@Dessix
Copy link

Dessix commented Mar 11, 2024

After seeing the usage scenario described by @kennytm in this comment, I am swayed - I was assuming further usage within existing match pattern scenarios, which would enhance if-let usages through a sort of inline guard, rather than focusing purely on an inverted match-order option.

This snippet with illegible repetition of @ convinced me of the merits of this RFC, and of the value in allocation of a new keyword:

if operation().or_else(another_operation) @ opt @ Some(_) {
    return opt;
}

This RFC provides legibility sorely missing from some patterns of if let, while being effectively as lightweight as a syntactic sugar. Unlike if-let chains, this new construct may be more amicable to logic chaining because it can bind in arbitrary conditionals, instead of requiring special-case control-flow constructs. This keyword allows variable declarations to take place in arbitrary boolean expressions, but the syntax for this is already thoroughly developed in match patterns. Ideally, if let would be a syntactic sugar which compiles to this.

@Pzixel
Copy link

Pzixel commented Mar 20, 2024

"Most code is usually read left to right" is an argument in favor of the is notation.

if let, chained or not, uses this "Yoda condition" order (10 == x instead of x == 10) that isn't idiomatically used anywhere else in the language. This bothered me since if let stabilization in 2015, and the matches macro partially rectified this (nobody would even think to stabilize the matches macro in the matches!(10, x) form), and is would finally fix this for good (besides just being more powerful in all other respects).

I mentally parse if let just as a form or let. In this case it's not more yoda than let x = 10. I understand what you're talking about, but with a slight change of mental model you can easily adapt.

I would simply said that languages shouldn't provide another way to bind variables because we already have too much of them. If we want to keep is then i'm fine with it - but then we need to remove not just if-let chains but if-let entirely. If this is too much - well, we shouldn't introduce this feature. Either one or another.

I would like to refer to C# language team principle of "minus 100 points" which applies here perfectly in my mind - this feature may be nice but it doesn't provide enough value to to get it into language. Citing Eric Lippert:

When we provide two subtly different ways to do exactly the same thing, we produce confusion in the industry, we make it harder for people to read each other’s code, and so on. Sometimes the benefit added by having two different textual representations for one operation is so huge that it’s worth the potential confusion.

But this isn't the case in my opinion. There is arguably a small benefit of slightly more natural reading, but this is it.

@camsteffen
Copy link

I mentally parse if let just as a form or let. In this case it's not more yoda than let x = 10. I understand what you're talking about, but with a slight change of mental model you can easily adapt.

Interesting... The two competing mental models are "if condition" and "assignment" because if let is an equal mix of those two things. The expected order of assignments is mutually exclusive with the "yoda condition" principle of conditions. So you have to violate one of the two expectations unfortunately. I think the "assignment" model is a bit higher in importance because of the fact that bindings are introduced.

@dileping
Copy link

I mentally parse if let just as a form or let. In this case it's not more yoda than let x = 10. I understand what you're talking about, but with a slight change of mental model you can easily adapt.

Interesting... The two competing mental models are "if condition" and "assignment" because if let is an equal mix of those two things. The expected order of assignments is mutually exclusive with the "yoda condition" principle of conditions. So you have to violate one of the two expectations unfortunately. I think the "assignment" model is a bit higher in importance because of the fact that bindings are introduced.

For me personally the current way of things is much more logical. Declare a variable with if let syntax, then check what's inside if the match was successful. && operator after the if let is sufficient for me and makes stuff done. Also, introducing yet another syntax in parallel feels redundant and confusing. IMHO, Rust is Rust and it doesn't have to be C# to be good.

@kurtbuilds
Copy link

kurtbuilds commented Mar 28, 2024

Yet another custom piece of code that gets unified/made obsolete by is is the assert_matches crate, which smoothes over the annoyance of assert!(matches!(..)) and gives better error messages.

assert_matches!(z, Some(_))) or assert!(matches!(z, Some(_))) becomes assert!(z is Some(_)). The alternative with if let is assert!(if let Some(_) = z { true } else { false }), which is... not great.

Sum types and pattern matching are a core aspect of what make Rust such a powerful language. All of these workarounds and use-specific macros & functions demonstrate what a huge hole the lack of refutable match expressions is in the language. if let and if let chains aren't expressions on their own, and only become expressions when you tack on { .. } else { .. }.

One way to quantify consistency is the degree to which a language is context-free. Rust, and (almost?) all complex languages are not context free, but they are mostly context free, because being able to look at nested elements independent of their context makes it easier to think about, read, and write the language.

if let chains break context-freedom in two ways. The if keyword turns all subsequent let keywords in the expression from an irrefutable matching keyword into a refutable matching keyword. It also turns let x = y from a statement into an boolean-valued expression.

The stewardship philosophy of Rust is to move slow and not break things. Rust 1.0 is not even 10 years old, a not insignificant, but still fairly short, history. This RFC isn't about removing if let, isn't about stopping if let chains RFC, it's not even about stabilizing is as the preferred way to do things compared to if let. The RFC is about reserving the keyword with the purpose of creating a nightly feature to try out a refutable matching operator/keyword. If the feature is implemented, and people prefer it in the real world, then maybe if let is no longer recommended, but would never be removed. Otherwise, if people don't prefer it, the nightly feature is removed, and maybe the keyword is dropped again in Rust 2027. Speaking for myself, in my real world coding, having a refutable matching operator/keyword would make my code easier to read & write in many places, so I'm really excited to have some kind of refutable matching construct.

I don't care if it's is or not, but I think for this RFC to be rejected and put to bed, there needs to be an alternative for refutable pattern match expressions that could progress to a nightly feature. I just don't see any reasonable alternative than is.

@kennytm
Copy link
Member

kennytm commented Mar 28, 2024

Yet another custom piece of code that gets unified/made obsolete by is is the assert_matches crate, which smoothes over the annoyance of assert!(matches!(..)) and gives better error messages.

assert_matches!(z, Some(_))) or assert!(matches!(z, Some(_))) becomes assert!(z is Some(_)).

Having is expression does not automatically make assert!(x is pat) look good yet... In the same way assert_eq!(x, y) still isn't replaced by assert!(x == y) (despite #2011 is already merged and available as #![feature(generic_assert)] which is still unstable after 7 years).

And #2011 can easily recognize assert!(matches!(x, pat)).

@JustForFun88
Copy link

if let, chained or not, uses this "Yoda condition" order (10 == x instead of x == 10) that isn't idiomatically used anywhere else in the language.

I could be wrong, of course, but you're contradicting yourself. Even your example uses normal mathematical notation: x = 10, which in the case of rust looks like (if, while) let x = 10. That is, the variable is on the LEFT, the expression is on the RIGHT.

@kennytm
Copy link
Member

kennytm commented Apr 9, 2024

@JustForFun88 you know if let x = 10 and if let 10 = x have totally different meanings?

    let x = 5;
    
    if let 10 = x {
        unreachable!();
    }
    
    #[allow(irrefutable_let_patterns)]
    if let x = 10 {
        assert_eq!(x, 10);
    }
    
    assert_eq!(x, 5);

@JustForFun88
Copy link

@JustForFun88 you know if let x = 10 and if let 10 = x have totally different meanings?

    let x = 5;
    
    if let 10 = x {
        unreachable!();
    }
    
    #[allow(irrefutable_let_patterns)]
    if let x = 10 {
        assert_eq!(x, 10);
    }
    
    assert_eq!(x, 5);

Yes, but you didn't read my message carefully. And please re-read @petrochenkov’s message.

@Yokinman
Copy link

Yokinman commented Apr 9, 2024

if let, chained or not, uses this "Yoda condition" order (10 == x instead of x == 10) that isn't idiomatically used anywhere else in the language.

I could be wrong, of course, but you're contradicting yourself. Even your example uses normal mathematical notation: x = 10, which in the case of rust looks like (if, while) let x = 10. That is, the variable is on the LEFT, the expression is on the RIGHT.

The fundamental readability difference is basically:

if let Some(x) = something() { .. }
if (let Some(x) = something()) is valid, bind it and then { .. }
if something is Some(x) { .. }
if something() matches Some(x), bind it and then { .. }

or like Pzixel said:

I mentally parse if let just as a form or let. In this case it's not more yoda than let x = 10. I understand what you're talking about, but with a slight change of mental model you can easily adapt.

Although, I think it is an intuition that has to be learned. A beginner has to understand let syntax and the fact that it accepts patterns (not just identifiers), but they also have to identify that if let is special syntax and that let isn't an actual expression.

Before you have that intuition, I think the first one feels backwards at best and esoteric at worst, whereas the second one reads like if something() == Some(x) at worst. But - this is also why I think is could work as a remain-in-condition style expression that doesn't bleed into the block; just short-lived local pattern matching.

EDIT: The more I think about it, maybe if let x = y {..} else {..} should have been let x = y do {..} else {..} (or something similar). It would feel like a natural extension of let instead of if - like obviously the binding persists into the block, because it's based around the binding which is obviously a statement (and not an expression). Whereas the is binding obviously shouldn't persist into the block - it's a standalone expression within the conditional.

@kennytm
Copy link
Member

kennytm commented Apr 9, 2024

@JustForFun88 The "Yoda condition" in #3573 (comment) is talking about the form if let 10 = x, which would be more natural if written as if x is 10. The comment mentioned nothing about variables, nor did x = 10 appear anywhere in that comment. The matches!(10, x) sentence is saying that no one will stabilize the macro if it were defined in the form matches!($p:pat, $e:expr). Perhaps you should reread it, to see there are no contradictions at all.

@JustForFun88
Copy link

@JustForFun88 The "Yoda condition" in #3573 (comment) is talking about the form if let 10 = x, which would be more natural if written as if x is 10.

I think you are wrong. Although if let 10 = x is generally a valid expression, no one writes it that way. Perhaps the expression if let 10 = x will look more natural as if x is 10, this is just a fake example.

Let's look at a more realistic expression let x = 10, here as in mathematics, the variable is on the LEFT, the expression is on the RIGHT.

Let's now look at if let Some(y) = f(x). We again see that everything is clear, and follows the same logic, the variable is on the LEFT, the expression is on the RIGHT.

@kennytm
Copy link
Member

kennytm commented Apr 10, 2024

@JustForFun88 the point is petrochenkov never brought up bindings in his comment, he was just talking about $expr is $pat is easier to read than if let $pat = $expr. It is fine if you think the bindings must appear on the LHS, but make it your own argument. Don't quote other people, and then bend their intention, and then accuse they are contradictory or wrong over arguments you've made out of thin air that the quoted person never made.

I think we've both made our points clear so I'll stop responding to this particular sub-thread about some.expr() is 10let some_var = 10.

@traviscross
Copy link
Contributor

@rustbot labels -A-edition-2024 +A-maybe-next-edition

We discussed the timeline of edition items in the lang meeting today, and this didn't make the list, so it seems unlikely this will happen for Rust 2024. We'll keep this in mind for future editions. Thanks to everyone working on this and providing helpful feedback here.

@rustbot rustbot added A-maybe-future-edition Changes of edition-relevance not targeted for a specific edition. and removed A-edition-2024 Area: The 2024 edition labels Apr 10, 2024
@dev-ardi
Copy link

Such an RFC with this much discussion should be probably be moved to its own repository for easier tracking. It will not make it for this edition but that means that we have 3 more years to think about this one.

@CEbbinghaus
Copy link

CEbbinghaus commented Apr 30, 2024

I am loving these discussions and wanted to add my 2 cents. The considerable upside for is matching would be the possibility of assigning the result, which is not possible with if let (this is also pointed out in #2497).

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 x is Some(x) || y is Some(y) which while theorized in #2497, has no actual equivalent.

The only functional "downside" of what is cannot do is replace let else statements (as pointed out here: #3573 (comment))

let Some(x) = y else { return; }

which to me seems entirely superfluous as is is not meant to replace either let or if let. (note that let else does not include the if keyword)

let chains however sit in direct opposition to this RFC as they aim to solve the same goal although with less flexibility. Unlike the is operator if let chains introduce an alternate function of the && operator as it no longer just does boolean algebra. This adds not only complexity to the compiler / tooling but also breaks programmers mental models as && now has 2 different functions.

Without let chains the is operator would become the defacto way of chaining multiple matches together which is likely the best place for it to start. While it can replace if let it probably shouldn't be explicitly encouraged by clippy. This would leave the developers the opportunity to decide for themselves which they prefer. (which also helps solve the "churn" problem)

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 is as pointed out here: #3573 (comment)).

Lastly I also think the keyword "is" is ideal as it complements the as keyword already present within rust.


on a more personal note, I think that the current if let & while let is needlessly confusing. While this has become accepted as normal within the rust community it does not fit in with any other modern programming language.

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

@mhatzl
Copy link

mhatzl commented Apr 30, 2024

While I understand the possible convenience of the is keyword for writing code,
I really have a hard time reading such code.
The same goes for as, but because of From<> Into<>, I rarely need and even want to use it.

I try to make it clearer why I have a hard time reading keywords like is and as.
Consider:

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 is has the form:

keyword(if) -> ident -> keyword(is) -> ident -> && -> ident -> keyword(is) -> ident

The code using let has the form:

keyword(if) -> keyword(let) -> ident -> = -> ident -> && -> keyword(let) -> ident -> = -> ident

One might notice that in the case with is, the keyword is always between two identifiers,
while with let, there are always non-identifier characters or keywords before let.
Because is consists of valid identifier characters and is fairly short,
there is not enough difference to the surrounding identifiers.

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
and some argue that this is better, because it reads out like a nice sentence.
But when reading code, I don't want to read the whole sentence.
I want to be able to skip through code until I get where I wanted to go.

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 let-chains and &&:

I thought, let ... returns true if the assignment succeeds, otherwise returns false.
Therefore, && still only operates with boolean logic.

At least to me this seemed natural.

@CEbbinghaus
Copy link

CEbbinghaus commented May 1, 2024

About the argument of bad mental model using let-chains and &&:

I thought, let ... returns true if the assignment succeeds, otherwise returns false.
Therefore, && still only operates with boolean logic.

At least to me this seemed natural.

I am unsure where this notion came from but it is entirely false. let does not return true and neither does if let. Changing that would be changing a core part of the language and likely be a backwards breaking change for most codebases. As now many statements would become implicit returns 1

let Some(val) = exp else {
    return false;
}

would now be implicitly returning true if the binding succeeds. The if let chain only use the && operator as it was picked as the most popular choice and not because it actually does a boolean comparison 2.

if let chains therefore also cannot do || expressions as that would require an additional RFC & Implementation. 3

I think this common misconception is the reason why some people prefer if let chains over the is operator. Without returning a boolean the most elegant way to set a boolean based on a match is still

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 func(if cond { val1 } else { val2 }) is entirely valid syntax.

if let chains break that by not being expressions4 but rather specialized syntax that applies in very limited cases & misappropriates an existing operator for purely aesthetic reasons. an if let statement cannot be used anywhere an expression can unlike the is operator which limits their usages. Having the consistency & flexibility that is provides would for example allow for

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 let or if let were to return a boolean upon successfully binding then this wouldn't even be a discussion as that would be objectively the better solution. However I don't think this is a possibility as that would be too big a backwards breaking change to a core part of the language.


Footnotes

  1. The example provided would not actually cause an implicit return but having let or if let return booleans could cause such problems.

  2. https://github.com/rust-lang/rfcs/blob/master/text/2497-if-let-chains.md#the-main-alternatives

  3. https://github.com/rust-lang/rfcs/blob/master/text/2497-if-let-chains.md#keeping-the-door-open-for-if-let-or-expressions

  4. if let is in fact an expression. but only as part of a whole if statement (same goes for if let chaining). Which makes the simplest alternative to the provided example: println!("Value is valid? {}", if let Ok(Some(_)) = val { true } else { false });

@Yokinman
Copy link

Yokinman commented May 1, 2024

I think this common misconception is the reason why some people prefer if let chains over the is operator. Without returning a boolean the most elegant way to set a boolean based on a match is still

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", ..}));

This seems to be one of the more common arguments in favor of is, and I think it's worth pointing out again that this wouldn't require introducing a binding like if let does. Instead the binding could be scoped within the following && chain, making is a unique utility in its own right (a 1:1 alternative for is_some_and since it's scoped - borrows are dropped after the condition).

Considering the sentiment against it as an alternative for let chains, it's probably the only chance is has of getting into the language anyways (maybe in 3 years).

@kennytm
Copy link
Member

kennytm commented May 1, 2024

when it could so easily be:

let bool_value = expr is Ok(Some({key: "foobar", ..}));

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 is expression there is no good reason why is it better than just using matches!().

@Yokinman
Copy link

Yokinman commented May 1, 2024

if we are to remove the binding ability of is expression there is no good reason why is it better than just using matches!().

There is one advantage I can think of, which is that you could combine a scoped is with let-chains. Where the former bindings are only used to produce the final let - not meant to be used later:

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 matches! would be just as good for everything else - probably easier to understand too unless you're doing a lot of consecutive nesting.

@CEbbinghaus
Copy link

CEbbinghaus commented May 2, 2024

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

@ssokolow
Copy link

ssokolow commented May 2, 2024

@CEbbinghaus That's fair... however, it must also be said that:

First...

Of those respondents who shared their main worries for the future of Rust (9,374), the majority were concerned about Rust becoming too complex at 43% — a 5pp increase from 2022.

-- https://blog.rust-lang.org/2024/02/19/2023-Rust-Annual-Survey-2023-results.html

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 lazy_static and once-cell... because every non-trivial dependency tree is very likely to contain one or both of them.

@CEbbinghaus
Copy link

CEbbinghaus commented May 2, 2024

@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. lazy_static & once-cell being fantastic examples of such.

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 if let chains will be stabilized removing the only functional gap that this RFC solves.

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 if let chains then it should be chosen based on those merits. But using the macro argument against this and not if let chains hinders us from getting the cleanest and most flexible version of Rust.

The if let proposal has already stated that they aren't going to stop because of this proposal and as such the only hope for is pattern matching is to be considered today without alternate macros and evaluated on its own merits.

RE

Of those respondents who shared their main worries for the future of Rust (9,374), the majority were > concerned about Rust becoming too complex at 43% — a 5pp increase from 2022.

-- https://blog.rust-lang.org/2024/02/19/2023-Rust-Annual-Survey-2023-results.html

I would actually argue that is simplifies the language (at least in some regards). It is true that it would add a new keyword and expression to master. And depending on the binding rules it could create some tricky detail in the edgecases. However it replaces a bunch of very specific methods (and some macros) with very simple logic (as pointed out here #3573 (comment)).

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 is teaches them 1 tool that they can apply in various ways to get the outcome they want.

@ssokolow
Copy link

ssokolow commented May 2, 2024

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 if let entirely" idea.

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.

@fluffysquirrels
Copy link

fluffysquirrels commented May 2, 2024

let is_small = a is None || a is Some(x) && x < 10;

can already be written as:

let is_small = matches!(a, None | Some(x) if x < 10);

The programmers mental model is strongest when there are small blocks like lego bricks that they can put together however they want.

I agree. The pattern matching syntax in match already exists in the language, and can already be used as a building block for this use case with matches!.

As @ssokolow points out, pre v1.0, there was perhaps a better case for is.

I also think is is not a great keyword for this. As pointed out in the if-let-chains RFC appendices, many people in the syntax survey confused it with Python's is, or expected it to have equality semantics. So I think matches or similar would be better.

I very rarely use matches! in my own code, and only even discovered it recently. I think this rarity is an indication that is does not fill a huge need. I think perhaps matches! can be made more prominent in the documentation.

@kennytm
Copy link
Member

kennytm commented May 2, 2024

@CEbbinghaus

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 is not a binary decision between "accepting every syntax change and the kitchen sink" or "reject all changes we have this_obscure_proc_macro! at home", it is a cost-and-benefit comparison to determine if a feature should be accepted or not. An is expression without binding certainly does not pull its weight. Your quoted example:

a is None || a is Some(x) && x < 10

also relies on the binding a temporary name x. And the non-trivial binding and its placement in an is expression is also what turned people off from this RFC.

(BTW this particular expression can be written simply as a < Some(10), consider choosing a different example).

It helps reduce compile time by removing yet another dependency

matches!() is in the the core library you don't need any dependency to use it

most time spent on compiling is codegen, whether using matches!(e, p) or e is p makes no difference in compile time after HIR generation.

and stops there being 5 different versions of the same functionality.

there is only a single version of matches!()

It is true that it would add a new keyword

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 is at least to r#is in the new edition

And depending on the binding rules it could create some tricky detail in the edgecases. However

the binding rule issue is not something you could "However" out

@petrochenkov petrochenkov mentioned this pull request May 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-maybe-future-edition Changes of edition-relevance not targeted for a specific edition. I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet