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

It has to be its own thing. There is no if statement, let statement, etc; it’s an if let statement. I don’t see where your complaint is.

There is a connection between if and if let only inasmuch as they both use the keyword if and run code if a condition is true. if let is closer to match than if. I’d say if let is an inflection of let, as per my previous post.

Using if x ~~ Some(x) { foo(x) } else { unreachable!() } and if x !~ Some(x) { unreachable!() }; foo(x) requires the exact same amount of special casing as does if let x = Some(x) { foo(x) } else { unreachable!() } and else let x = Some(x) { unreachable!() }; foo(x).

The point of these constructs is the binding, just the same as with let. If you don’t care about the binding, I agree, use the matches! macro, and !matches! is fine.

Feel free to message me directly if you’d like to continue this line of discussion; I’d like to keep this thread focused on the refutable let counterpart to the existing conditional let.

1 Like

Do we expect refutable lets to be used for anything other than enums (I’m not sure what else they could be used on)? If so this seems like the least-friction way to do what refutable let is ‘meant’ to do, namely swap which side of the match that a variable gets added to the scope of.

Are we considering implementing some of these as a macro like the old try!? I’ve made a quick attempt at else match here (not a very pretty one, thanks to the macro_rules! syntax restrictions, and not very powerful given how limited $p:pat fragments are. I think these would work better as proc macros.).

There exists the guard crate with implements at least guard!(let PAT = EXPR else { BLOCK });. I’m looking into implementing some of these proposed syntaxes as a proc macro this weekend, hopefully.

I really like the match for the diverging case, though I do think it should be optional. I’ll try implementing unless!(let PAT = EXPR [match] { .. }); first. Repo will be linked as soon as I have something to share.

Feel free to beat me to it, though!

1 Like

Got a bare-bones else match proc macro working here (not sure if it’ll be useful, but it was fun to write!).

1 Like

I think it’s important that it starts with let to make it clear that the binding is introduced in the surrounding scope.

3 Likes

(Likely off-topic) One advantage making let $pat = $expr as an expression (or introducing $expr is $pat or $expr ~~ $pat) is that it can be likely()ed e.g. in this expression:

            if unsafe { intrinsics::likely(mul.is_some()) } {
                return self.iter.nth(mul.unwrap() - 1);
            }

we can get rid of the unwrap():

            if unsafe { intrinsics::likely(let Some(m) = mul) } {
                return self.iter.nth(m - 1);
            }

It would be nice to maintain this behavior, and that's why the other proposals that don't maintain this are marked with this as a known problem.

This is an interesting application. I would not be against introducing a matches operator with strict rules about when and where it introduces bindings. But that's a larger step than a refutable let, which feels like a good extention to the language as-is, and I'm still iffy on making let serve that role.

(Interesting note: if ~~ uses something along the lines of @comex's rules, then it could replace let entirely. I'm just really iffy on side-effected operators though, especially ones that impact the language.)

We hit 50 responses to the poll, so here’s a mini-analysis of the results so far and a runoff poll for those interested.

2018-02-07

  • “Stick to the status quo” got exactly half of the votes so far. This seems interesting to me, but also means that the idea of a refutable let has some significant support. In hindsight maybe I shouldn’t have included the option, so that half of the respondees could voice which alternative they’d like if we introduce one.
  • let ... else took half of the remaining vote (including preceded by <keyword>). This seems by far to be the crowd favorite, even with the grammar ambiguity. If it weren’t for the ambiguity, this would be the obvious choice.
  • 12% of the vote went to <keyword> let (including else let). Upon further consideration, I think that unless in this position could be contextual and only require single-token lookahead; $:ident let is never currently legal code.

So that said, here are the two syntaxes I’d like to runoff:

let ... else match

let Ok(x) = make_x() else match {
    Err(e) => {
        log!(e);
        bail!(e);
    }
}
do_something_with(x);

(Requires special-casing an if expression to always greedily consume the else, rather than a fully unambiguous grammar.)

unless let ... match

unless let Ok(x) = make_x() match {
    Err(e) => {
        log!(e);
        bail!(e);
    }
}
do_something_with(x);

(Requires a new contextual keyword or using else here which is human-ambiguous.)

  • let ... else
  • unless let

0 voters

I’d also like to guage opinion on one other factor before finalizing RFC draft for initial discussion:

  • Make the match part optional, infer _ => for the diverging block
  • The match part should always be required, just type _ => if you don’t care about the value

0 voters

I don’t see any improvement over

let x = if let Ok(x) = make_x() {x} else { bail!(); };
// or
let x = match make_x() {
    Ok(x) => x,
    Err(e) => bail!(e),
};

The only syntax that adds ergonomics and would be worth it is

let Ok(x) = make_x() else { bail!(); };
4 Likes

There’s still an unfortunate ‘sour-spot’ (what’s the opposite of a sweet spot?) where you want to do a complicated match that you don’t want to write twice, but you want easy access to the failing cases. Let’s use something nice and complex like syn::Expr, and assume I only care about the syn::ExprWhile case. Then it’s much nicer to do this:

let Expr::ExprWhile(ExprWhile {
    ref attrs,
    ref cond,
    ref body,
    ..
}) = expr else match {
    Expr::ExprWhileLet(_) | Expr::ExprLoop(_) => 
        bail!("use normal while loops!"),
    other => bail!("Expected a loop, got {:?}", other),
};

Than it is to try and extract out the bindings in a normal if let or match and return them. For reference, I think the equivalent of the above is:

let (attrs, cond, body) = match expr {
    Expr::ExprWhile(ExprWhile {
        ref attrs,
        ref cond,
        ref body,
        ..
    } => (attrs, cond, body),
    Expr::ExprWhileLet(_) | Expr::ExprLoop(_) => 
        bail!("use normal while loops!"),
    other => bail!("Expected a loop, got {:?}", other),
};

Which I hope you agree would get annoying to maintain if you wanted to change the match.

1 Like

Maybe this is just because I’ve never used syn, but for me both of those are so awkward to read that the difference between them feels negligible.

1 Like

Huh, can m @ Something { ref x, ref y } work?

For these constructs, I generally prefer a form such as

let expr_while = match expr {
    Expr::ExprWhile(expr_while) => expr_while,
    Expr::ExprWhileLet(_)
    | Expr::ExprLoop(_) =>
        bail!("use normal while loops"),
    other => bail!("Expected a loop, got {:?}", other),
};
let expr_while = &expr_while;
let &ExprWhile { ref attrs, ref cond, ref body, .. } = expr_while;

That’s what I get for not checking things on the playground… fixed, thanks! Also fair point on actually doing separate matches. I suppose once an enum gets too complex most libraries will move the struct variants out to their own types, like syn has - are there any good examples where that’s not the case, i.e. interesting examples that still keep enum struct variants?

I use struct variants usually when some variants have more fields and no logical ordering, like for state machines.

But there I’d prefer to use

let (foo, bar) = match {
    State::A { foo, bar } => (foo, bar),
    State::B { .. } => return Err(..),
};
do_something(foo, bar);

kind of construct, because it’s easier to adjust when one has to react to multiple variants:

let (foo, bar) = match {
    State::A { foo, bar } => (foo, bar),
    State::B { .. } => return Err(..),
    State::X { foo } => (foo, None), // new variant
};
do_something(foo, bar);

But that doesn’t mean there aren’t any situations where it could be useful.

One syntax I think I haven’t seen yet is

let if Some(x) = expr else {
    log!();
    bail!();
};

This one

  • Doesn’t need a new keyword.
  • Still has let as current-scope-binding keyword.
  • Has an early if implying some form of condition.
  • Has some symmetry with if let (if let <cond> <scope> <else> into let if <cond> <else>; <scope> because the initial let means “current scope”).
  • Makes the else more expected at the end due to the if directly after the let.
4 Likes

Just as a general remark, my perception is that whichever special sugar is chosen (if any), I don’t see any reason not to consider a kind of “chainability”:

let TheVariantWeNeed(val) = expression1 or else expression2 or else expression3 or else ...

When we expect to match a certain variant, it’s not impossible or rare we have substitute expressions. All of these may fail, but the situation is not always as binary as match expression with the expected pattern or leave the golden path.

4 Likes

Here is a real world example of code that is unrelated to bubbling up errors. That is not simplifiable without ad-hoc methods or making the code flow way more inscrutable. This needs a let...else expression of some sort.

3 Likes

For the record, I still want to solve this by actually making let a bool-valued expression, so that if !let works and so do more complex expressions, e.g.

    if (let Some(bar) = foo) &&
       (let Some(baz) = bar.do_something()) &&
       baz.is_good() {
        // use baz
    }

or

    if !(let Some(x) = bar) && !(let Ok(x) = baz) {
        return;
    }
    // use x

Even though it’s a bit less pretty, I like how this approach can replace what would otherwise be three different features:

  1. The use case of this thread;
  2. If-let chaining (RFC: “Support && in if let expressions”)
  3. A way to get a bool value of whether a pattern matches (RFC: “Add a matches!(expression, pattern) macro.”)

See also my old proposal for how to solve the variable scoping issue.

4 Likes

If that’s open source code, can you point me to where it is so that I may patch it to use match instead of if let? It won’t solve all your problems but will save you three lines.

From my experience with Erlang’s fancy scoping rules, this kind of code is highly confusing for readers. You can’t even describe the scoping in prose easily anymore.

Question. With regard to the let else ambiguity, has it been discussed to make the “return” or “break” be part of the keyword:

let Foo(x) = <expr> else return <expr>;

or

let Foo(x) = <expr> else break <expr>;

What I like about this is that it enforces the mentality that this syntax is intended for “inverting” rightward drift and nothing else. The main problem I see here is that sometimes you want to panic!. Of course you can do that but it feels a bit silly to have to specify a return or break that is dead code:

let Ok(x) = value else return {
    panic!("this should not happen")
};

The other concern I can think of about this idea is that it might be confusing why else return 22 works there but not if foo { .. } else return 22 here (of course, this is the key that makes it unambiguous).

1 Like