I tend to avoid if let because it doesn't feel readable/clear to me what it means. It sounds like it's a let statement, but in reality it's a shorthand for the match statement which is quite different. A match statement can fail, but let statements cannot. if let confuses those two concepts.
I have three suggestions for alternatives:
// 1
if match self.value => Some(value) {}
// 2
if self.value is Some(value) {}
// 3
if self.value matches Some(value) {}
I would prefer 1 or 2 personally, but all 3 are an improvement over if let in my opinion
I don't think you will have a lot of success of changing this because removing if let would be a breaking change and have two constructions that only differ in one keyword of syntax seem a bit much.
Furthermore, there is some thoughts that let x = ... could be an expression evaluating to a boolean itself has been brought up.
They're not as different as you might think! let statements can pattern match in the same way that match or if let can:
enum Example {
Data(i32),
}
let x = Example::Data(123); // Wrap the data.
let Example::Data(y) = x; // Unwrap ('destructure') the data via a pattern.
println!("{}", y); // This prints '123'.
The only restriction is that the pattern in the let must be irrefutable - i.e. it must always match successfully. So this would not work:
enum Example {
Data(i32),
Oops,
}
let x = Example::Data(123);
let Example::Data(y) = x; // error[E0005]: refutable pattern in local binding: `Oops` not covered
println!("{}", y);
This is why the if let syntax was used - it literally is just a let which can fail to bind[1]. I agree that it's maybe not the most intuitive thing at first glance, though.
I'd argue that let Example::Data(y) = x isn't really matching anything though, unlike if let. I'd classify that as destructuring, not matching. let makes me think set this to this, which isn't what if let is for. It might be similar in how it works under the hood maybe, but the concepts feel pretty different.
The fact that let must be infallible is exactly what makes if let unintuitive (as well as it looking like an assignment is used as an expression)
My 2 cents is: if let brings me joy. I wish OCaml had introduced it decades ago: OCaml's let allows non-exhaustive patterns (which can fail at runtime), but Rust forces you to either use an exhaustive pattern (with let) or decide what to do (or do nothing) if it doesn't match (with if let). This is just so great.
With let-else you can use let to match fallible patterns. Likewise, if let can match infallible patterns, and while currently there aren't lot of reasons to do that (mainly temporary lifetime extension, although I generally see match being used for that) with if let chains there will be actual reasons to use infallible patterns in the middle of an if let chain in order to reuse a temporary value in the later steps of the chain.
When one begin with Rust, the intuition expects if let .. else destructure some way the all different cases of an enum, so one firstly try with a Result<T> for managing errors; but latter, after trials and searching online one realizes this can be only done with match.
It is like if let .. else were exclusively designed for managing None (From an Option<T> for example) due else{} does not receive any value from the processed enum.
Basically one expects destructure as quickly and clean as possible Result and Option for proceeding to work with the data (without using unwrap()? ), and an if...else have the more comfortable "visuals" due a mere { } across lines.
Are you saying it would bring you less joy to have the syntax changed? This change also wouldn't change anything about exhaustiveness, it's just meant to make it more clear and intuitive
FWIW, the RFCs for if let and while let both cited Swift for precedent. You might also want to review those RFC PRs for their design discussion, including alternative syntax.
My immediate reaction to this is that we could probably have
// res is a Result
if let Ok(value) = res {
// happy
} else let Err(reason) {
// unhappy
}
It doesn't say else let Err(reason) = res because it only makes sense semantically if you're assigning from the same source in both branches. But the problem is what if there's a lot of code in the happy block? You'd have to scroll up and down looking for the assignment source. Of course this is also a problem with
match res {
Ok(value) => {
// happy
},
Err(reason) => {
// unhappy
},
}
but what that means, I think, is that the only way else let [contextually irrefutable pattern] improves on a match is by having one fewer level of braces and indentation. Which isn't nothing! But it makes me want to look for a change we could make to match to get rid of that extra brace level. Hm. We need some kind of delimiter between the match subject and the first pattern. Maybe reuse the fat arrow?
match res =>
Ok(value) => {
// happy
},
Err(reason) => {
// unhappy
},
This hypothetical match block is in tail position of a function, so the parser knows when it's reached the end of the match clauses because the next thing after that trailing comma is another close curly brace. If the match block isn't in tail position then you have to put a semicolon either right after, or instead of, the trailing comma.
What about something more or less like this. The only important detail is all happens inside a function, so one needs and can use return if needed.
// res is a Result
// opt is an Option
let data1 = res match Ok or Err(e){ log(e); return Err("Custom err"); }
let data2 = opt match Some or None { log("failed"); return Err("Custom err"); }
// now one works with all the destructured datas along the function
Desugaring with other example:
let mut withdata3 = true;
let mut withdata4 = true;
let default = 10;
let data3 = res match Ok or Err(e){ withdata3 = false; default }
let data4 = opt match Some or None { withdata4 = false; default }
// is:
let data3 = match res {
Ok(v) => v,
Err(e) => {
withdata3 = false;
default
},
};
let data4 = match opt {
Some(v) => v,
None() => {
withdata4 = false;
default
},
};
Another example:
let data = res match Ok or Err(e){
res2 match Ok or Err(e2){
log(e, e2);
return Err("res and res2 failed");
}
}
// is:
let data = match res {
Ok(v) => v,
Err(e) => {
match res2 {
Ok(v) => v,
Err(e2) => {
log(e, e2);
return Err("res and res2 failed");
},
}
},
};