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

@clarfonthey clarfonthey Feb 16, 2024

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

@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

@jendrikw
Copy link

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

At this scale, support for cargo fix should be strongly encouraged.

@nikomatsakis
Copy link
Contributor

Syntax wise, I wonder about x match pattern -- e.g., if x match Some(_) {. Reads a bit oddly I suppose.

@cramertj
Copy link
Member

cramertj commented Dec 3, 2024

@nikomatsakis Your Scala is showing 😉

Though, that's actually a surprising choice to me, as I'd expect if x match ... to parse as a postfix match, as in

// 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 if and postfix match is confusing, and we already ban braced expressions in if. But it'd be surprising if postfix match had a different meaning in this position.

@nikomatsakis
Copy link
Contributor

Love me some Scala. I still want _ closures. :)

@the8472
Copy link
Member

the8472 commented Dec 17, 2024

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.

@GoldsteinE
Copy link

GoldsteinE commented Dec 17, 2024

x is Entry::Vacant(_) (or, for readability, x is hash_map::Entry::Vacant(_), since Entry is way too general) is much less nice than x.is_vacant() though.

@the8472
Copy link
Member

the8472 commented Dec 17, 2024

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.
Those functions bloat documentation and mean additional work for us.

And ideally it'd be combined with enum type inference, e.g. x is _::Vacant(_). Or just x is Vacant(_), which the RFC proposes.

@Yokinman
Copy link

x is Entry::Vacant(_) (or, for readability, x is hash_map::Entry::Vacant(_), since Entry is way too general) is much less nice than x.is_vacant() though.

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.

@kennytm
Copy link
Member

kennytm commented Dec 18, 2024

Or just x is Vacant(_), which the RFC proposes.

I can't find anywhere in this (3573) RFC proposing making x is Vacant(_) legal without use std::collections::hash_map::Entry::Vacant;.

Inferred type #3444 for x is _::Vacant(_) is orthogonal to the is expression feature.

@safinaskar
Copy link

@kennytm wrote:

/*1*/ let x = 9999;
/*2*/ let y = Some(4);
/*3*/ if y is Some(x) && x > 0 {
/*4*/     println!("x1 = {x}"); // x1 = 4
/*5*/ }
/*6*/ if y is Some(x) && x > 0 || cheat_code_enabled() {
/*7*/     println!("x2 = {x}"); // x2 = 9999  !?
/*8*/ }

Just by adding that || on line 6 causes the x on line 7 refer to the outer variable on line 1 instead is surprising.

For if let chains this kind of miss is impossible (and the let keyword makes the scope very clear), but an is expression can appear in any arbitrary place for expression!

I think that, even if x is not bound, the name x must still "pollute" the entire expression statement so use of x will generate at minimum a warn-by-default lint.

I totally agree. I propose the following solution: $expr is $pat should be allowed in the same places let $pat = $expr is allowed only (but not instead of let statement, of course). This solves this problem. $expr is $pat and let $pat = $expr then will mean absolutely same thing. As a side effect, the language becomes easier to learn. You may say: "But what if I want to use $expr is $pat in other places?" Easy! Just define the following macro (you can even add it to standard library):

macro_rules! scope {
  ($a:expr) => { if $a { true } else { false } }
}

And now you can use scope!($expr is $pat) (and even scope!($expr is $pat && ...)) everywhere! @kennytm's example becomes:

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 scope! clearly delimits variables' scope.

Also, you can additionally allow $expr is $pat (and ... && $expr is $pat && ...) inside of { ... }. This is very natural, because { ... } usually delimits scope. Now you will be able to do this:

if { y is Some(x) && x > 0 } || cheat_code_enabled() {
    println!("{x}"); // Works, 9999
}

an_expression() is x;
```

# Reference-level explanation
Copy link
Contributor

@traviscross traviscross Dec 20, 2024

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.

@Skgland
Copy link

Skgland commented Dec 20, 2024

if y is Some(x) && x > 0 || cheat_code_enabled() {
    println!("{x}"); // Hard error, "is" is not allowed here
}

Rather than disalowing the is I think it would make sens
to have the bindings introduced by is shadow existing bindings unconditionaly
and treat them as possibly-uninitialized.

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.

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.