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

I’d like to add another option to that list:

let match Variant0(x) = expr {
    Variant1(..) => ...,
    Variant2(..) => ...,
    ...
}
// `x` is in-scope here

I think this is preferable to let ... else for a couple of reasons. Firstly, it’s just as powerful since you can use a single match branch to blindly capture all possibilities other than the pattern you were looking for. eg:

let match Foo(x) = expr {
    _ => return,
}

Secondly, when you test to see whether expr matches your patten you’re already doing a match on it. So it makes sense to have access to the result of that match in the “else” clause. Otherwise people will end up writing code like this:

let Ok(x) = my_result else {
    let err = my_result.unwrap_err();    // ewww! unwrap!
    ...
}

Edit: Although maybe match let would be better than let match. That would make it look like if let and while let although that might be a bad thing since match let binds variables in the code after the block, not in the block.

12 Likes

I actually really like this idea of providing match arms within the diverging block. Though this could be provided without the match keyword provided, doing so clarifies intent.

I personally don’t have a real opinion on let match versus match let. The order of match let is more consistent to if let/while let, but since this adds its binding to the containing scope, starting with let seems beneficial as well.

I’ve added the option to the OP, but I’m not allowed to edit the poll (to avoid invalidating results).

Interesting idea- removes some rightward drift at the cost of adding some in other cases. I wonder if there’s a good syntax for making it optional- something like this maybe?

unless let Foo(x) = some_foo() match {
    Bar(y) => return ...
};

Or

let Foo(x) = some_foo() else match {
    Bar(y) => return ...
};

The second one plays into the idea of else match that I think has popped up before.

2 Likes

Another approach is to lean on hypothetical is operator: https://github.com/rust-lang/rfcs/issues/929#issuecomment-285602496

if some(foo) !is Foo(x) {
    // no x in scope here
}

// x in scope here

cc @petrochenkov

1 Like

I love @petrochenkov 's suggestion a lot. We really need a “subject-verb-object” structure to express “expression-matches-pattern”. is is less verbose than matches , I think it is a great option on table.

Details about $expr is $pat (or let $pat = $expr as a boolean expression) can be found in discussions of RFC #2260. One big problem of is is how bindings are made. Without name bindings it is not sufficient to replace let … else. 2260 compare is with several other languages (C#, Kotlin, TypeScript), but:

  • Swift’s $pat ~= $expr does not support name binding

  • In C# 7’s $expr is $pat, a declaration statement will be inserted before the current statement, i.e. the following two statements are equivalent:

    return f(c is float d);
    // <=>
    
    // declaration inserted
    float d;
    // when we evaluate `c is float d`, it will be expanded to
    bool _result = c is float;
    if (_result) {
        d = (float)c;
    }
    return f(_result);
    

    This is possible only because every type has a default value in C#. The variable d will be “leaked” after calling f, and has a value of 0 if c is not a float. This treatment is not possible in Rust.

  • Kotlin’s smart cast ($expr is $ty) and TypeScript’s type guards (e.g. $expr instanceof $ty) is not used for pattern matching (destructuring). They alter the type of $expr so that type-specific fields will become available.

There is an attempt to create a precise scoping rule in https://github.com/rust-lang/rfcs/pull/2260#issuecomment-354525996.

1 Like

I’m not sure what’s the best option here (though I like the let match family of options), but I definitely feel like this pattern comes up a lot, and it’d be nice to have a good fix for it.

4 Likes

I think we should consider syntaxes which act purely as modifiers on patterns (converting a refutable pattern into an irrefutable pattern).

One possible example:

let {
    panic!()
} unless Some(foo) = bar();

This adds a new form of pattern:

<irrefutable_pat> = <expr:!> unless <refutable_pat>

Would also be usable as:

match x {
    A => println!("A"),
    B => println!("B"),
    panic!("unexpected") unless C => println!("C")
}
3 Likes

Interesting concept. What makes

match e {
    A => "A",
    B => "B",
    unreachable!() unless C => "C",
}

any better than

match e {
    A => "A",
    B => "B",
    C => "C",
    _ => unreachable!(),
}

though? Correct me if I’m wrong, but I can’t think of any location other than let where a single pattern is used rather than allowing one of many to be selected, and a _ arm serving to make it exhaustive.

I’m pretty sure that specific syntax would require using a full keyword instead of a contextual one as well, in order to avoid ambiguities.

This doesn't work, you also need the if block to be diverging, otherwise we can have an uninitialized x

if foo !is Some(x) {
    // can't use 'x' here, good.
    // but we choose to fallthrough
}
let y = x;  // the `x` here should be invalid

Unless you only permit return/break/continue, knowing whether an expression is diverging (e.g. has type !) requires type checking, which would be too late to determine if x should be in scope or not.

The idea is that x is always in scope (from name resolution point of view), but it may be potentially uninitialized and if it's uninitialized, then access to it results in an error reported by some control/data flow pass performed after type checking.
(I never proposed bindings from is to be in scope outside of their statement though, so in my scheme is is not a solution for the let ... else problem.)

2 Likes

I was under the impression that refutable let's else branch is required to be diverging under any proposal? Like, this is the main idea behind the feature: you handle exceptional cases by returning, and deal with a happy case using straight-line code :slight_smile:

Yes, refutable let requires the else branch to be diverging, but a simple is expression places no such requirement.

Hm, it does places such requirement? Like, you’ll get “variable is unitialized” error at compile time if there’s no divergence, and everything would be ok if there is divergence.

:confused: I’d expect it follow Swift’s guard let ... else which requires a diverging block.

IIRC there is also suggestion allowing it to be non-diverging but still initializes the x:

let Some(x) = foo else { Some(4) };
// equivalent to `x = foo.unwrap_or(4);`

I didn’t remember suggestions making it conditionally uninitialized though :sweat_smile:

1 Like

What I don’t like about proposed variants of the syntax is that they look too much like a multi-line value for a regular let:

let Some(val) = {
   some();
   code()
};

is visually quite similar to the opposite case:

let Some(val) = val else {
   some();
   code()
};

And the let {…} unless <pattern> is so far the clearest one in this regard :+1: (the different meaning of the block is easy to see by the lack of the pattern in let {.

I also propose a variant: do {} unless let <pattern>.

1 Like

I really don’t like putting the pattern at the end of the diverging block, because it’s the condition that determines whether it will execute. Would a different initial keyword be a strong enough marker? (e.g. unless let Some(val) = val { ... })

1 Like

Just throwing this out there:

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

The if ... let is supposed to be an easily recognised and familiar pattern.

I use blocks for let values often, and with the diverging block it doesn’t look good:

unless let Some(val) = {
   success();
   code()
} else { 
   failure();
   code()
};

It looks silly without the else between the value and the block, but with else it looks too if-let-like.

unless Some(val) = val return {
   ...
}

Require return or, where appropriate, break.

1 Like