RFC idea: let ... else, easy early returns when pattern matching

I'm talking about the whole problem, not only when there are two else clauses. The fact that even deciding whether or not it's ambiguous was hard is a red flag to me.

1 Like

I do agree that it's not the most readable solution. I'd like to have something a little more readable.

Suggestions welcome for how we can do that without conflicting with existing syntax or introducing any new keywords.

In particular, I'd love to have something that "announces" the presence of the else before the RHS expression.

1 Like

I definitely feel this pain. Here's another proposal (perhaps a bit out there):

let Some(value) <= foo else {

}
  • + Doesn't conflict with existing syntax (I think)
  • + Doesn't introduce new keywords
  • + Distinguishes from other syntax before the RHS
  • - Introduces new "operator"
  • + Doesn't repeat the newly introduced variable name
  • ? Clarifies assignment rather than conditional

I like this idiom except that the identifier thing has to be written three times. Could we introduce some sugar? I don't have good generic design in mind but for example:

let thing = match an_enum {
    <= SomeVariant(@),
    _ => return Err("err"),
}

By introducing <= $pat match arm syntax. "@" is a sigil for the "hole" and result value of the match expression is value of that hole if $pat is matched.

2 Likes

I just use Kotlin/Groovy name it: https://github.com/rust-analyzer/rust-analyzer/blob/870ce4b1a50a07e3a536ab26215804acdfc9ba8a/crates/ra_assists/src/ast_editor.rs#L95-L103

Isn't this a job for a macro, à la snafu::ensure?

let_ensure!(SomeVariant(thing) = an_enum, "an_enum should be of type SomeVariant");

// Expands to
let thing = if let SomeVariant(thing) = an_enum {
    thing
} else {
    return Error("an_enum should be of type SomeVariant")
} 
// do something with thing
2 Likes

I'd be wary of using the less-than-or-equal operator for this purpose. Note that <- is reserved and could be used here.

But if I recall the discussion from way back then, one of the objections was that the following would be valid, but confusing:

let Some(var) = if test() { foo() } else { bar() } else { baz() }

I.e., else being used twice. Changing the assignment operator won't fix this.

Edit: Just saw @dhm has already brought this up.

1 Like

There's also this: https://github.com/rust-lang/rfcs/issues/2616

2 Likes

That RFC proposes if !let = ... and if let != ... syntax. However, this is suboptimal, because the ! is easy to miss when reading code. The proposed if !let syntax has different semantics and control flow than the existing if let, so the difference should be more visible.

Another possibility would be a context-sensitive keyword:

let Some(x) = foo or { ... };

or no keyword:

let Some(x) = foo { ... };

Personally, I like the original proposal with else most.

The fact that even deciding whether or not it's ambiguous was hard is a red flag to me.

There are a lot of possibilities to write code that is hard to understand. When you write unreadable code, that's your fault, not the fault of the language.

1 Like

This is untrue as-is. When a construct by itself and by default looks confusing, that's definitely a design problem. It's not like there aren't better or worse alternatives in syntax… And anyway, by that logic, why introduce something into the language, just to then instruct people not to use it?

1 Like

I think you misunderstood me. The following looks ambiguous (at least to a human):

let Some(var) = if test() { foo() } else { bar() } else { baz() };

The following does not:

let Some(var) = foo() else {
    baz()
};

Also, we can make the first expression more readable by adding parentheses.

3 Likes

Swift has a quard statement. It may be worth copying it verbatim, with an edition if necessary. Rust's if let is copied from Swift already.

    guard condition else {
        statements
    }

Honestly, I prefer spelling it as guard let (and have suggested as such previously).

I think the current position of the language team (or at least Centril) is to see how far extending let's usability as an expression gets us. We're getting if let $pat = $expr && let $pat = $expr currently, and there is rough design for making let usable more generally as an expression in if conditions, including naturally supporting if !(let $pat = $expr).

(Specifically, the case where the let "expression" "evaluates" to false would be required to diverge, and $pat would be bound in the containing scope, i.e. guard let.)

As I've said before, I rather like unless for this, given the diverging requirement. I don't like it in other languages where it's just if!, but the "block must be -> !" gives it a reason to exist.

That allows for things like unless i > 0 { continue } as well, since it's useful to set preconditions for a block for values as well as types. (That's what assert! does, for example, so it's clearly valuable.)

An advantage of if !let syntax is that it can be combined with the if let syntax. For example:

if !let Some(x) = foo && let Some(y) = bar {
    // `y` is in scope here
    // this block has to diverge
}
// `x` is in scope here

Edit: Actually that idea is broken. Nevermind.

Unfortunately, I don't think that can work. What happens when x = None; y = None; in that case?

3 Likes

There's nothing wrong if we would only accept || and prohibit let matching alongside with !let.
This seems to be the "inversion" of principle that if-let-chain uses.

std::ops::Try is already an option here.

let thing = an_enum?;

If std::ops::Try was a bit easier to implement, and made a bit generic, then this could become:

let thing = an_enum.info_result::<Ok=SomeVariant>.error_chain(|| "an_enum should be of type SomeVariant")?;

Which is kind of gross, but I think it's a superior direction than relying on the if/else pattern matching when looking to extend the syntax.

1 Like

It would be nice to allow initializing missing bindings in else clause instead of diverging e.g.:

if !let Some((a, b)) = x {
    a = y;
    b = z;
}

or

let Some((a, b)) = x else {
    a = y;
    b = z;
}
2 Likes

Sounds like the perfect task for Option::unwrap_or_else().

I would definitely not like to see code like that in the wild, it's needlessly cryptic.