Background
Rust 1.65.0 stabilized let-else
statements. In the discussion on r/rust, u/intersecting_cubes made the following observation:
let-else is useful, but only in limited cases where you don't want to inspect the other branches. Most of the time, I do want to inspect them, though. For example, if I'm unpacking a Result, I probably want to log the error instead of just returning or breaking. This requires binding the Err branch, which let-else won't do.
The more I thought about it, the more I agreed with this point. It seems like it would be quite common to want to use values in a failed pattern before diverging. This would still retain all of the benefits of let-else
statements (reduced repetition, etc.), but you would have a more powerful else
branch.
Proposal
My (very early, only moderately considered) proposal is to add a let-else-match
statement. The match
statement would need to cover all patterns that weren't covered by the let
binding.
I imagine something like the following:
let Ok(x) = foo() else match {
Err(e) => {
println!("Error: {:?}", e);
return;
},
};
println!("Got: {}", x);
If our call to println!
was a constant, then we could have used the new let-else
statement. However, if we want to use the Err
value, we need to match on it. As of today, the best option is to revert back to a normal match
statement, with all the duplication that entails.
let x = match foo() {
Ok(x) => x,
Err(e) => {
println!("Error: {:?}", e);
return;
},
};
println!("Got: {}", x);
Another Example
let (Some(x), Some(y)) = (foo(), bar()) else match {
(Some(x), None) => {
println!("We need both, not just {}", x);
return;
},
_ => return,
};
println!("We have {} and {}", x, y);
As of today, this would need to be written as:
let (x, y) = match (foo(), bar()) {
(Some(x), Some(y)) => (x, y),
(Some(x), None) => {
println!("We need both, not just {}", x);
return;
},
_ => return,
};
println!("We have {} and {}", x, y);
This is adequate but repetitive.
Possible Concerns
Does this lead to ambiguous syntax?
I don't think there's a possibility for ambiguous syntax since a normal else
statement has to 1) come after an if
statement, and 2) has to be followed by {
, not the match
keyword.
Is this actually a common use case?
This is the part that I'm least confident in. I don't know how often this will come up. Since let-else
was just barely stabilized, I haven't even used that yet. If anybody wants to go through a small crate and see how many places could take advantage of this that could be helpful.
Does this make the language more complicated?
Yes. Is it worth it? I think it adds a nice feature without making the language more complex, and I think the behavior is fairly intuitive. If I saw this code in the wild and hadn't been closely following Rust for a while, I suspect that I would shrug my shoulders, think "I didn't know you could do that", and expect it to work as I have described. I think that my interpretation of the behavior would be the most common interpretation by far.
The only alternative interpretation of the semantics I can think of is thinking that the match
lived inside an implicit else
block. However, Rust has a pretty consistent history of not letting blocks be implicit, so that interpretation would be assuming a significant deviation from that pattern.
Do we really need to do this?
This is simply syntactic sugar and doesn't allow us to write any code that was impossible to write before. It does seem like a natural extension of the let-else
statement.
What alternatives are there?
let Ok(x) = match foo() else {
...
};
^ This seems weirder to me than my proposal, but I guess it could work.
let Ok(x) = foo() match else {
...
};
^ This doesn't flow as well for my English brain. I also think that the match
keyword should be closer to the patterns it is matching against than the else
keyword.
let Ok(x) = foo() else {
Err(e) => {
println!("Error: {:?}", e);
return;
},
};
^ I believe this would make both programmatic parsing and human reading much more difficult. You don't know whether it's a "normal" let-else
statement or a let-else-match
statement until you see if there are arrows or not. This also might subject to more serious ambiguities; I haven't thought about it for very long.
Conclusion
After thinking about this for about a day, it seems like it could be a helpful addition to the Rust language. I've tried to come up with reasons why it wouldn't work/would be too costly/aren't a good fit for the Rust language and haven't come up with anything.
I look forward to hearing your feedback! If there's an obvious (or subtle) reason why this isn't a good idea, please feel free to point it out!