-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a let...else
expression, similar to Swift's guard let...else
#1303
Conversation
This would be awesome to have. I often want to check error conditions and early-return at the start of a function and have the rest of the code be clear and un-indented. I'm usually pretty conservative when it comes to new syntax sugar, but I've wanted this exact feature so many times. Importantly, it can un-nest the core logic of even a simple function: fn foo() {
if let Some(x) = bar() {
// body goes on for a while
// .
// .
// .
// .
// .
// .
} // wait, why am I doubly nested here?
} vs fn foo() {
if !let Some(x) = bar() { return; }
// body goes on for a while
// .
// .
// .
// .
// .
// .
// end as usual
} In my experience this reduces the cognitive load of reading such code. The error conditions are out of sight and out of mind while reading the body. In the former case the error condition lingers in the form of nesting, and possibly an else block, and it only gets worse in many real examples with multiple things to be unwrapped and matched. Before the inevitable bikeshed sets in, I'll just say it's not that important whether we really use |
This looks cool! I wonder though: if the primary use case would be fn main() {
let maybe: Option<usize> = None;
if let None = maybe {
println!("It was 'None'.");
}
let result: Result<(), ()> = Err(());
if let Err(_) = result {
println!("It was 'Err'.");
}
} ? Definitely neat for more complicated types! |
@zslayton Your examples don't allow the user to get the "success" value out of the |
Doesn't look bad, though I'm concerned about the implemention. Detecting divergence is easy enough (it more-or-less already exists, you'd just not get particularly nice error messages) but it's the rest that I'm worried about. It can't be implemented by desugaring, as it has an effect on the entire function, not just the little part it occupies syntactically. As such, this isn't really an extension of the current Instead, it is much closer to |
This feels too different from |
While The "must diverge" aspect is also unusual, but it shows that this proposal is firmly about returning early in "error-like" cases. We already have this for the
I have an experiment that does 2~4 with Sufficiently Advanced Generics. 5 could be added easily. It also converts to/from For the 1 case, maybe |
I find it disturbing that this new form of
Also, I don't understand why there should be a requirement about the divergence of the body, except for the fact that it would be a common use case for this construction. As an alternative, you can add that the same result can be achieved using this construct:
|
@olivren The |
I'll present an alternative syntax that might get the point across better: // Unlike in a regular let, this pattern must be refutable, not irrefutable.
let Some(a) = x else { return; }
// use `a` here The above would be equivalent to this RFC's: if !let Some(a) = x { return; }
// use `a` here |
@tsion Oh I missed that |
I like @tsion's alternate syntax. FWIW, in clippy we have https://github.com/Manishearth/rust-clippy/blob/master/src/utils.rs#L266, which works pretty well I'm a bit uneasy that this introduces a branch without indentation. It's a bit jarring to read. |
I feel like it's the same as: if !some_important_condition() { return; } And the error handling block could be placed on the next line and intended as normal, etc. |
Yeah, but returns/breaks/continues are pretty well known across the board. This is ... strange, and cuts into our "strangeness budget as @Havvy mentions. |
I have to say I'm surprised at the references to the strangeness budget, because I find this to be a very natural extension of the language (at least with a sufficiently bikeshedded syntax). The essence of the idea is this: Then you need a block to handle the failure case, hence the (Note that this is a different sense of " |
The main "strange" bit is that it introduces nonlinear flow, in a novel way. return/break/continue are known to behave that way. They also have obvious points to jump to. fn foo() {
if something{
if !let ... {}
if !let ... {}
}
} if one of them fails, where does it jump to next? It's not immediately obvious. return/break/continue are known constructs, and everyone knows where they jump to. This is completely non-obvious on the other hand, especially for people coming from other languages. |
@Manishearth But the bodies of those |
Oh, right. In that case, I'm mostly okay with this. |
To be specific, for anyone else reading, |
(Or |
I like the @tsion’s example above and think the syntax could be expanded even further to subsume
|
In the first,
|
@burdges This is highly complicated, without any precedent, requires rustc to create subsets of enum types and whatnot on the fly (because the non-diverging path may have This also still seems completely unrelated to |
Yes it be complicated. Right now, it's an argument against doing any |
I really want this RFC to be accepted in its original form. Then, when you write: if !let Some(x) = get_some() {
return Err("it's gone!");
}
println!("{}", x); // and other useful stuff
// more stuff which translates into if let Some(x) = get_some() {
println!("{}", x); // and other useful stuff
// more stuff
}
else {
return Err("it's gone!");
} Yes, it's a bit uncommon that instead of been declared in block scope |
I feel like the |
IMO, Swift
They're specifically designed for check and forced early-exit logic. And IMO, new relatively unfamiliar keyword
using of IMO, just using
Series of Or just using nothing would be better for extreme brevity by sacrificing readability.
As it's clear that the name |
In a similar vein to this RFC I've seen proposals to allow
I've also seen people propose allowing
I think the lesson here is that we could unify and generalize all these proposals to allow a whole algebra of binding conditionals where A
With this system you could write stuff like
|
|
@canndrew Interesting idea. AFAICS rust shouldn't allow matches on both branches of an if-statement. As an example the following code looks meaningful, but also slightly confusing. if !let Some(x) = foo || let Some(y) = bar || let y=2 {
// Do something with y
return
}
// Do something with x Nitpick: In your example you cannot do stuff with |
Yeah, it's actually impossible with just
I'm not sure I follow. If I guess this shows that this syntax has the potential to be too confusing though, |
It seems to me like we need a real guard statement in addition to It would be very unintuitive (and would probably lead to much confusion) if
added While this is still useful even if it doesn’t add bindings to the outer scope, it doesn’t solve the nesting problem that guard statements address. I agree with @eonil that Swift’s guard style is the most appropriate:
I think it’s abundantly clear what’s going on here. Additionally, it should be possible to have a guard statement without a let:
|
Used this feature yesterday in real C# code: private Task ValidateTransactionAsync(ILogger logger, RabbitMessage message)
{
if (!(message is ValidationRabbitMessage validationMessage))
{
logger.Error("Cannot validate message {@ValidationMessage}", message);
return Task.CompletedTask;
}
switch (validationMessage.Message)
{
case Request request:
return ValidateRequestAsync(logger, request, validationMessage.TransactionHash);
case RequestStatusUpdate requestStatusUpdate:
return ValidateRequestStatusUpdateAsync(logger, requestStatusUpdate, validationMessage.TransactionHash);
default:
throw new InvalidOperationException($"Unknown message type '{message.GetType().Name}'");
}
} Still waiting this feature. |
It's been 3.5 years since this was closed as postponed. In my opinion this RFC is still very relevant, so I think it might be time to reopen it. |
@JelteF It doesn't fit on this year's roadmap, so probably doesn't make sense to reopen right now. That said, we're only a few months away from setting a 2020 roadmap, so consider whether there's a good theme you could propose in a blogpost to make it something that would fit next year. |
Having just discovered this issue, here is how I think of it:
I think these problems can be addressed separately. The second issue is much easier to address with new syntax, I think: Solving the first issue would, I think, require some new syntax for indicating what pattern the
Both new features together would result in:
As an interim solution for the first issue, it should be fairly easy to create one or more macros to provide ergonomic ways to translate patterns into
...would desugar to something like:
... or, for unwrapping arbitrary enums, it could be a
|
The if w == x? {
y
}
{
z
} |
@kennytm Ah. I hadn't thought about that. |
Not quite the same, but I've found myself using 1.42's
This feels really clunky, and is not as powerful as what's presented in this RFC. Would love to see this RFC re-opened. |
In #1303 (comment), 5 years ago:
The ? operator has been established for a long time, and AFAIK no one came up with a better idea. Isn't it time to revisit this RFC? Otherwise, I would expect other criteria to be given until when it should remain postponed. |
I still feel quite good about this feature too. i would like it if someone would produce a short summary of the grammar challenges we encountered, I remember them being quite minor. |
I haven't re-read all the hundreds of comments here, but to summarize the most prominent syntactic hurdle: The RFC proposes the syntax The most natural compromise is to say that the former parse must always be chosen (not just for compatibility with existing code, but also because it's what users would naturally expect). The syntax for this feature would need to be changed from |
I am actively drafting a modernized RFC for this. You can follow along in Zulip if you'd like: https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/topic/.60let.20pattern.20.3D.20expr.20else.20.7B.20.2E.2E.2E.20.7D.60.20statements |
In case anyone from here missed it and is still interested, new RFC has been up for a few days at #3137 |
Rendered
Note: The initial version of this RFC used the syntax
if !let PAT = EXPR { BODY }
. In the current version this has been changed tolet PAT = EXPR else { BODY }
for reasons discussed below.Update: There is a new version of this RFC in #3137.