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

One of the tools that I miss most to make code easily understandable in Rust code is easy early returns/continues. They are not are not well supported together with pattern matching. This makes code often highly nested. Which breaks the flow of the code for me, because I cannot simply read it from top to bottom anymore.

The most common thing I do to keep code easy to understand in Python, Go and C (languages I develop professionally in) is the following pattern:

if x < 10 {
    return Error("x should be more than 10")
}
// do something with x

The reasons I like this pattern are:

  • you do any checking at the start of the function and in all code after that you know that x is fine
  • it's very clear that it errors out on that check
  • code doesn't get deeply nested.

With a simple if this is possible in Rust, but when pattern matching is involved (which it often is) the best I could come up with is this:

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

This is a pretty verbose syntax for this pattern. That's why I would like to propose a new syntax. Something like this:

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

Before opening a full fledged RFC I wanted to get a bit of feedback on this idea. So some questions:

  • Am I missing a better way to do this pattern (or to get its advantages)?
  • Do you think this would indeed be a useful addition to the language?
  • Any better suggestions for the syntax?
6 Likes

There's an RFC already at https://github.com/rust-lang/rfcs/pull/1303

3 Likes

Often, a match actually reads better than if-let in this case:

let thing = match an_enum {
    SomeVariant(thing) => thing,
    _ => return Error("an_enum should be of type SomeVariant"),
} 

EDIT: rust-analyzer even has an assist to convert if-let to match: #454.

12 Likes

See also: [Pre-RFC] Alternative syntax for pattern-matching based branching.

1 Like

That's actually inferior to what's being proposed here. let-else would shorten (somewhat) a common (somewhat) idiom. That other proposal basically just duplicates the complete semantics of if let with a different syntax.

(That said, I'm not a fan of present proposal either, because I'm starting to acquire syntactic diabetes from all this sugar. Yet I'd take this let-else as a valid ergonomic improvement in contrast with that other thread, because it actually adds some very minor compression and semantics. But exactly duplicating already existing functionality has no objective benefits other than a very personal "I prefer the colon to if let".)

3 Likes

The question here is. Do you care what "thing" is?

If so, then the let thing =match ...makes a lot of sense. if you've gone to the trouble of putting a value in a variant, you're likely to want to use it. Especially after checking it's there.

You're probably gonna want to unwrap it later anyway,

If not, Option and Result already have the is_err, is_ok, is_some and is_none methods. which handle the case you don't care what the thing is. It's not too hard to add those to a custom enum.

Or even (if you're feeling super hacky.) proc macro up an auto is_<variant>() for deriving.

2 Likes

I like the idea, sadly that syntax will not be possible:

let () = if false { () } else {
    return true;
};
return false;

would lead to an ambiguous parsing (and if it compiled, it would not be clear whether that block would return true or false).

On the same vein, this time without parsing ambiguity but just not very readable code, we could get:

let SomeVariant(x) = if true { foo } else { bar } else {
    return "Madness";
};

We will need some special syntax to disambiguate,

let $fallible_pattern:pat := { $matched_expr:expr } else {
    $fail_expr:expr // (of diverging type: !)
};

By using a special token (which could be =? or bikeshed_assignment_sigils), we are allowed to requires braces, which in turn remove the ambiguity of else.

@dhm I don't think that's ambiguous. If there's an open if, then else has to pair with that if. Otherwise, it pairs with let.

You can elide else { () }, leading to if expressions without a trailing else. These if expressions evaluate to (), but the parser has no knowledge of types so it cannot disambiguate based on that.

1 Like

I understand, but I'm saying that the parser can just always pair else with the innermost unmatched if or let, unambiguously.

1 Like

Good point, giving "priority" to completing a dangling if expression does solve the ambiguity for the parser, my bad.

…although it's not at all intuitive or well-readable.

1 Like

I don't think it'll come up, in practice. You can't in general omit the else on an if on the right-hand side of an assignment, unless the expression has type ().

Also, I'd recommend a style lint in clippy saying that if you write let x = if ... else ... else ...; then you have to put braces around the first if ... else ....

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