Pre-RFC: Allow pattern matching after guard clause


#1

I had opened this as an issue on the Rust repo itself https://github.com/rust-lang/rust/issues/45978 . I’ll post what’s there as of this writing and I’ll let the conversation continue normally from there.

Original Post

I’ve opened a separate issue for a Rust style guide on Guard Clause. What this issue is about is that Rust currently doesn’t recognize when a path has already been handled by a Guard Clause.

I want to do the following:

use std::fs::File;
let file = File::open("program.cfg");

if file.is_err() { return Err(Error::ConfigLoadFail); }
let Ok(f) = file;

let Ok(f) = file is a nice usage of pattern matching. But Rust complains as it doesn’t know I’ve already accounted for the error path:

error[E0005]: refutable pattern in local binding: `Err(_)` not covered
   --> src/lib.rs:166:7
    |
166 |   let Ok(f) = file;
    |       ^^^^^ pattern `Err(_)` not covered

error: aborting due to previous error

I really look forward to using pattern matching with guard clauses rather than using unwrap() which I find a bit of an eye sore.

I love Rust, keep up the good work :slight_smile:

kennytm commented:

You could write this as:

let f = match file {
    Ok(f) => f,
    Err(_) => return Err(Error::ConfigLoadFail),
};

or, since file is a Result, not instance of an arbitrary type:

let f = file.map_err(|_| Error::ConfigLoadFail)?;

If we supported rust-lang/rfcs#1303 (let ... else) this could be written as

let Ok(f) = file else {
    return Err(Error::ConfigLoadFail);
};

I think the compiler should never support the original code by OP as this means we need to inspect the implementation of is_err to determine it will always return false on the Ok branch to determine the let Ok(f) = ... is irrefutable.

I’m not sure if we should allow:

if let Err(_) = file {
   return Err(Error::ConfigLoadFail);
}
let Ok(f) = file;

this looks reasonable, but should require a proper RFC to clarify the control flow interaction.

I responded:

I’m not sure how the internals for Rust checking paths are implemented, but I’m suggesting this more as a relative Guard Clause thing rather than a Result thing.

In the example above the is a conditional check on file with a immediate exit with return in the code block. The very next item within the same scope is the same item file being pattern matched for assignment. So this looks like it wouldn’t be too hard to implement.

What I mean by not strictly a Result thing is, for example, imagine if File.open returned an Option instead. Then my recommendation would look like:

if file.is_none() { return Err(Error::ConfigLoadFail); }
let Some(f) = file;

I do love the map_err example you provided. I didn’t know about that. I think my feature request would be more readable and beginner friendly rather than using map_err to achieve the same result.

kennytm commented:

What I mean is that the compiler should not be allowed to peek into the definition of is_err() or is_none() to know that let Ok(f) = or let Some(f) = is acceptable, i.e.

if file.is_none() { return; }
let Some(f) = file;
// Unacceptable, the signature `fn is_none(&self) -> bool` tells nothing 
// about the typestate of `file`

but

if let None = file { return; }
let Some(f) = file;
// Acceptable, we know that `Option` can only be 
// `None` or `Some(_)`, and the `None` case will not reach here
// so we are sure that `file` can only be `Some(_)`
// and thus the pattern is irrefutable. 

my last response:

if let None = file { return; }
let Some(f) = file;

Yes. I find that as very acceptable. Thank you.


#2

In the last years type systems are being used to statically type dynamically typed languages, in Flow (JS), TypeScript (Js), MyPy (Python), Typed Racket (Scheme), etc. To make them more ergonomic their type system need to be refined and flexible, so they often contain flow typing (invented in Whiley language?). Now you’re suggesting to start introducing something like flow typing in Rust :slight_smile: Once (and if) enum variant become distinct types in Rust, your idea becomes true flow typing.


#3

I suggest using a different example as motivation for this feature, because specifically for aborting on Err there’s already a syntax sugar:

let file = File::open(path)?;

I’d love to revive let … else {}.

I’m not sure about let conditionally succeeding because of another expression earlier in the flow. One hand that may be a common-sense expectation, but OTOH that’s quite “magic” adding whole another dimension to type inference.


#4

Oohh, if let ... else {} sounds really nice. The ergonomics for pattern matching through a series of error conditions (that pattern or return early) are pretty painful today, as far as I’ve been able to figure out.

Here’s one motivating example:


#5

Do I have enough here for an RFC? How long should this simmer in Pre-RFC? What considerations and applications do I need to take into account to create an RFC?

This will be my first RFC so I’d be grateful for the help in making it.

I believe very firmly that this would improve Rust and future generations will be grateful for it. I go a bit more in depth on the importance of Guard Clause here: Idiom: Prefer Guard Clause over Nested Conditionals. But in summary I believe it will make Rust far more clear, readable, and approachable by beginners.


#6

At least for me, many of the early suggestions in this thread seemed like non-starters or required too much heroic effort from the compiler to be a net win, but the if let ... {} else {} suggestion seems like a slam dunk, so I would guess that the simmering phase is complete.

I’m not sure what the etiquette is on posting RFCs during the impl period (or the post-impl period floodgate-opening that I suspect is going to happen soon), so hopefully someone with more clout can comment on that.


#7

The let … else {} is outside the scope of what this (to be) RFC is trying to achieve. As interesting as it may be it does not provide the true flow typing leanardo mentioned and doesn’t create the clarity and newbie friendliness my proposal would bring. That focus is one of the big focuses in Rust right now in trying to make Rust easier for first timers. My proposal will do just that.