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

Can somebody explain, maybe with a concrete example of something where it poses a problem, why the syntactic ambiguity is actually an issue? The only case where the ambiguity comes up is if you are using let ... else on something of type (), which there is no imaginable reason to do and it doesn't even work (doing the same with if let errors out with "irrefutable if let pattern" and presumably this'd do likewise), so it's absolutely clear that it should always be parsed the other way. There's an ambiguity in theory, but not in practice.

I'm not saying we shouldn't discuss it or anything, but it seems to be regarded as some kind of quasi-blocker and I don't understand it.

I guess if you also allow break-with-label and continue and also throw if we ever add that... but panic!() isn't the only function with a return type of !, e.g. in a small command line app I'd call the "print help and exit" function. (Of course you could do the same workaround but as you say, it's weird.) I guess my point is that the ways to short-circuit/exit are more varied in practice than one might assume in theory, so special-casing the few built-in ones doesn't feel as justified. Also e.g. macros with one of the control flow keywords inside them... (or the type inference fallback to ! that's going to happen, so maybe something like Err(x)? would work too...)

2 Likes

That's a good point. I remember that now. =)

I will say that at this point I feel fairly persuaded that we should adopt let x = y else ...;, where ... must diverge.

Ever since we rejected the PR, I've suddenly been noticing a need for this all the time, and I've also been less satisfied with the current proposal of let x = match y { Ok(v) => v, _ => ... }.

3 Likes

That seems fairly common in rustc (well, bug! not panic!, but).

The return one is interesting, but wouldn't it give a dead code warning? I guess it could be exempted from the warning.

This positive impression is the push I needed to get the revived RFC in a share-ready state. I agree that the ambiguity is an academic one only, and one where the “other” case doesn’t pass typeck 99.99% of the time and the other 0.01% of the time doesn’t make sense for a logical flow.

let ... else does seem to be the cleanest option, so the RFC will start out in favor of that formulation, though of course will list the other proposals as alternative realizations.

I do really think that the else match idea brought up in this thread is a logical extension to the original proposal, and one that makes handling the divergence case cleanly a lot easier. Though I’m not sure whether it should be part of the initial RFC or a listed possible extension. The primary use-case of destructure-or-diverge doesn’t need it, but any time you want to capture what failed to destructure for a better error, it’s going to be more work. I still think the refutable let expresses intent better than any other option available today.

The hardest part is of course finding small but motivating examples. Please, if anyone has some, share! I should have a sharable pre-RFC this weekend (but if I don’t, anyone feel free to beat me to the punch).

1 Like

This is an interesting point. I wonder how often that is the case, seems measurable. (It is -- of course -- a forward compatible extension, but it's worth taking a look to see whether not handling match is excluding a lot of cases.)

This. I don't want to reopen the can of worms that was Add 'else match' blocks to if expressions. by phoenixenero · Pull Request #1712 · rust-lang/rfcs · GitHub. (So I'd also rather it not be let ... else match ... either.)

3 Likes

About that ambiguity with

// This is actually non-sense but just purely looking at the syntactic composition, it's confusing
let Some(val) = if x { () } else {
    panic!("wops");
};

Instead of extending the else keyword with return or break, we could also just use else do?

let Some(val) = if x { a } else { b } else do {
    panic!("wops");
};

And in the unlikely event that it does compile, clippy will tell you that you probably shouldn't be doing what you're doing: Redirecting to https://rust-lang.github.io/rust-clippy/v0.0.187

I'm convinced.

We could prohibit if/if let expressions at that position (perhaps also all control expressions like loop, while, for), just like you can’t use a struct literal in a for: for i in S { x: y } { … }.

If you need to use if, wrap it in (…) or {…}:

let Some(val) = (if x { a } else { b }) else { panic!("wops"); }
//              ^                     ^

@kennytm IINM the issue is that you can currently write:

let foo = if x { a } else { b };

With the letelse feature, the syntactic ambiguity could allow this to be parsed as:

let foo = (if x { a }) else { b };

Viewed from this perspective, not only is that interpretation nonsensical, but backwards compatibility also determines that we must parse it the other way.

But since this code is currently allowed (and common!), we also can’t require parentheses in it without breaking compatibility; and if we commit to parsing it the current way (which we must), then I believe that also wouldn’t leave any ambiguity in your slightly-larger and not-currently-legal example (even without parentheses).

3 Likes

I think we better to introduce a new contextual keyword instead of dealing with an ambiguity. One additionall option for it is “otherwise”:

let Some(val) = foo otherwise return;
let Some(val) = foo otherwise break;
let Some(val) = foo otherwise { .. };

It’s a bit lengthy, but reads quite nicely. We could even allow “otherwise” block to return value which is guaranteed to match pattern:

let Some(val) = foo otherwise { Some(0) };

Why? What problem could the ambiguity cause, even if we always know which way to resolve it?

Great example!

Could we continue to treat let foo = if x { a } else { b }; as the current meaning, and error out if we see let foo = if x { a } else { b } else { c(); };?

That is, let $pat = if … always leads to an irrefutable let.

(Let me check if this has been discussed before in the original let … else RFC)

Edit: I’ve rescanned the discussion of RFC 1303 and did not find any suggestions making let _ = if _ { _ } else { _ } else { _ } an error.

5 Likes

I feel like ? is the established syntax for doing early return, and this proposal would have similar functionality to ? but with completely different syntax. In the spirit of somehow using ?, I’d propose:

let? Some(x) = expr else { diverges } 

Also I’d propose that expr cannot be an expression that takes a block such as if, match, for, while and loop since it would be confusing to know what the else refers to, but it could be just a block as in let? Some(x) = { expr } else { diverges }.

2 Likes

How about something simpler, such as:

if not let Some(val) = expr {...}

A contextual keyword not and a required return or break. The similarity to if let here is the whole point. They are twin constructs – the positive and the negative match.

Edit: Thanks, @scottmcm. Requiring one of these “diversion” keywords is indeed not sufficient. One should instead avoid using a complex construct in the deconstruction expression – so as to not add unnecessary confusion. Such a keyword also makes the construct more of an exception in the ordinary Rust syntax, and more unlike its twin, if let Some(val) = expr {...}.


Here’s an extrapolation of this idea that I find interesting:

while not let Some(val) = attempt_something() {
    println!("log failure...");
    if ... {
        // exiting the loop is possible with a matching value:
        break Some(default);
    }
} 

I think there's been too much focus on the return examples here. The main use for this I have in mind uses continue as its -> ! operation, where involving ? would be extremely misleading.

I don't like the ones with this kind of restriction because they prohibit other diverging forms: continue, loop, panic!, or other -> ! methods.

5 Likes

What is the syntax ambiguity with guard <pat> = <expr> else { <diverging block> };?

Yeah, I worry that having a non-pattern-matching form of let .. else syntax will encourage people to throw away useful information. eg. It'll be easier to write

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

than

let x = match foo {
    Ok(x) => x,
    Err(e) => panic!("there was an error: {}", e),
}

So people will opt for the former and the error message will be lost.

More generally, if you end up with a piece of information in your hands there's probably something you can do with it, or else you should be deliberate about dropping it. So I'm wary of giving people a syntax form that just silently drops the information stored in the other cases.

2 Likes

Using guard or any other contextual keyword alone will have ambiguity when used together with slice patterns (which is going to be stabilized soon).

#![feature(slice_patterns)]
fn main() {
    let mut guard = [[0]];
    guard [0] = if true { [0] } else { panic!() };
}

You need a keyword besides guard i.e. guard let $pat = $expr else { $block };.

I find it fun that you included the “academic” ambiguity there as well. There are four ways that could parse:

let [0] = (if true { [0] }) else { panic!() };
let [0] = (if true { [0] } else { panic!() });
guard[0] = (if true { [0] }) else { panic!() };
guard[0] = (if true { [0] } else { panic!() });