[Pre-Pre-RFC] Reviving Refutable Let [Runoff]


There is no ambiguity if guard becomes a normal keyword in a future epoch, right?


The variable name guard is used quite a lot in Real Code™ unlike catch or dyn, so I don’t support keywordizing it when guard let is perfectly cromulent.


I don’t see how guard instead of let resolves the let PAT = if cond { a } else { b }; / guard PAT = if cond { a } else { b }; ambiguity. The problem is the else, not the start of the syntax. Using a new keyword allows you to resolve the ambiguity differently (parsing it as let PAT = (if cond { a }) else {b} would be backwards incompatible) but you still need to resovle it and it’s not clear to me why that would be desirable.


Oh right, can’t believe I forgot guard is massively used as a variable name, even in Servo…


Would we be better off if foo.unwrap_or_else(|| break/continue/return) just worked? Edit: Would the common case of Result and Option be better off?

It would be a single line closure starting with one of those control flow primitives. For backwards-compatibility with return we need to come up with new syntax, for example unwrap_or_else(do || return bar) where do is some keyword.

The semantic desugaring would be something to the effect of:

fn func(x: Option<i32>) -> i32 {
    x.unwrap_or_else(do || return 1)
// Desugars into:
fn func(x: Option<i32>) -> i32 {
    let flag = false;
    let x = x.unwrap_or_else(|| { flag = true; mem::uninitialized() });
   if (flag) {
       return 1;



I don’t think this is any different from if let, where the same example becomes

let x = if let Ok(x) = foo { x } else { panic!("there was an error") }

And again, I think all the examples using simple, two-variant enum matches are giving the wrong intuitions for this feature. To copy my MIR example from above (updated to what seems to be the current syntax proposal), when you’re looking for a particular complex pattern, there isn’t an “other side” that makes sense to talk about:

let Statement {
        kind: StatementKind::Assign(lvalue, Rvalue::BinaryOp(_, lhs, rhs)),
    } = bin_statement
else { continue };

Yes, that’s ignoring non-assignments, assignments from unary operations, etc. But that’s the point.

I do like the unless form of things, since as I’ve said I think it’s helpful even more non-pattern-matching conditions, the same way that assert! is phrased at what should hold, not the condition on which to panic!. But I don’t seem to have been convincing enough about it, it would need the new epoch, and I have no problem with let ... else after glaebhoerl’s points.

No, because of cases like the one earlier in this post where the there isn’t any reasonable unwrap_or_else-like method that could be put on the type. I think the various forms of ? handle the things with unwrap* fine. (We could potentially even put the unwrap methods on Try, though that’s a conversation for another day.)


This is perhaps more of a brainstorming suggestion, but since it hasn’t been brought up so far (I think): how about a full merging of let and a partially abbreviated match that contains the assignments within. For example:

let match bin_statement {
    Statement {
        let source_info,
        kind: StatementKind::Assign(let lvalue, Rvalue::BinaryOp(_, let lhs, let rhs)),
    },  // '=>' not required, but allowed - e.g., for extra calculations/assignments
    _ => continue

Granted, this would be a much larger language addition than a mere else after a let, but would be more flexible, and also IMHO somewhat more readable, because it highlights the bound variables.


Initial RFC draft is up! (Link is pinned to commit; switch to master to see most up-to-date version.)

I’ve written the Motivation and Reference-Level sections so far. I expect the Rationale/Alternatives section to be half of the RFC’s final text given how many options we have available for syntax, and alternate features that could express the same idea.


An initial review of the RFC draft:

if let takes a refutable binding and binds it to a new scope

This is not accurate, if let accepts irrefutable patterns in RFC accepted Rust, see: https://github.com/rust-lang/rust/issues/44495

In current Rust, the obvious way to handle this is to use if let to destructure the Ident:

I’d encourage you to use non-diverging examples. Otherwise, an RFC reader might get the impression that panics are encouraged. The RFC seems very tailored around the needs of custom derive macro authors (and compiler hackers), which are comparatively a small set of people.

Personally, I believe that let .. else { .. } can often be solved by providing .extract_foo() operations that extracts the fields out of a variant fallibly with some Try monad such as Option or Result.

This is better, because the error handling case is closer to the failable operation.

Why the use of match there? I’d just continue with if let.

The solution using if let would needlessly indent the happy path a level

Yes, but just a single level. The problem does not scale with if let pat = expr && cond {..} (which I think we should do some version of irrespective of this RFC).


I’ve done some digging for data of code that could take advantage of this in rustc. It’s incomplete but I got about 50 samples of code that could use this feature. The conclusion is that Option and Result are the common case, but not the 90% common case, more like 70%. So the other cases are significant, but still alternatives specialized for Option and Result should be considered.


Hmm, good point. From time to time I ponder the idea of an “opt-in” TCP closures in order to help address problems like “using ? in closures” and so forth. The idea would roughly be to have some kind of closure notation where the closure returns a “break/continue/return” enum, which gets propagated through the inner layers, and the compiler inserts code to interpret that when it comes to the right time.

I don’t love this, but do expr(...) |foo| { ... } has been floated, so i’ll run with it. This would mean: create a TCP-style closure C; invoke expr(..., C) with C as the last argument. Take the return return of that and “process” it to reflect control flow.

Then you would write:

let x = do foo.unwrap_or_else || {

Hmm Hmm Hmm. I really, really don’t like that syntax :slight_smile: but I do sort of like the idea of leveraging TCP here.


This look quite complicated to follow and comprehend at scale, and this still means we need at least one helper function per case where one would want to use guard let. Many use cases I encounter for this feature in Servo rely on neither Option<T> nor Result<T, E>.


Yeah, true. In any case, TCP is a Big And Uncertain Thing to Design. Given that we already have if let – which could also be expressed with TCP! (I mean, basically anything can) – I’d not be opposed to adding some extension sugar to that form in the meantime.


There seems to be two syntax features at play here, trailing closure syntax and do. Is it the trailing closure syntax you don’t like, do, or just the whole deal? I suppose trailing closure syntax might fit better if the feature were implemented generator-style rather than like try (based on matching on a return value), though I had the impression the latter was what you were suggesting.


I dislike both parts of the syntax, and I believe this is a vastly different feature than what we are trying to achieve here anyway.