Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a let...else expression, similar to Swift's guard let...else #1303

Closed
wants to merge 15 commits into from
1 change: 1 addition & 0 deletions text/0000-if-not-let.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Moved to [0000-let-else.md](0000-let-else.md).
149 changes: 149 additions & 0 deletions text/0000-let-else.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
- Feature Name: let-else statement
- Start Date: 2015-09-30
- RFC PR:
- Rust Issue:

# Summary

Introduce a new `let PAT = EXPR else { BODY }` construct (informally called an
**let-else statement**).

If the pattern match succeeds, its bindings are introduced *into the
surrounding scope*. If it does not succeed, it must diverge (e.g., return or
break). You can think of let-else as a “refutable `let` statement.”

This simplifies some common error-handling patterns, and reduces the need for
special-purpose control flow macros.

# Motivation

[if-let expressions][if-let] offer a succinct syntax for pattern matching
with only one “success” path. This is particularly useful for unwrapping
types like `Option`. However, an if-let expression can only create bindings
within its body, which can force rightward drift and excessive nesting.

let-else statements move the “failure” case into the body, while allowing
the “success” case to continue without additional nesting.

## Example

For example, this code written with current Rust syntax:

```rust
if let Some(a) = x {
if let Some(b) = y {
if let Some(c) = z {
// ...
do_something_with(a, b, c);
// ...
} else {
return Err("bad z");
}
} else {
return Err("bad y");
}
} else {
return Err("bad x");
}
```

would become:

```rust
let Some(a) = x else {
return Err("bad x");
}
let Some(b) = y else {
return Err("bad y");
}
let Some(c) = z else {
return Err("bad z");
}
// ...
do_something_with(a, b, c);
// ...
```

## Versus `match`

It's possible to use `match` statements to emulate this today, but at a
significant cost in length and readability. For example, this real-world code
from Servo:

```rust
let subpage_layer_info = match layer_properties.subpage_layer_info {
Some(ref subpage_layer_info) => *subpage_layer_info,
None => return,
};
```

is equivalent to this much simpler let-else statement:

```rust
let Some(ref subpage_layer_info) = layer_properties.subpage_layer_info else {
return
}
```

The Swift programming language, which inspired Rust's if-let expression, also
includes a [guard-let-else][swift] statement which is equivalent to this
proposal except for the choice of keywords.

# Detailed design

Extend the Rust statement grammar to include the following production:

```
stmt_let_else = 'let' pat '=' expr 'else' block
```

The pattern must be refutable. The body of the let-else statement (the
`block`) is evaluated only if the pattern match fails. Any bindings created
by the pattern match will be in scope after the let-else statement (but not
within its body).

The body must diverge (i.e., it must panic, loop infinitely, call a diverging
function, or transfer control out of the enclosing block with a statement such
as `return`, `break`, or `continue`). Therefore, code immediately following
the let-else statement is evaluated only if the pattern match succeeds.

The following code:

```rust
let pattern = expression else {
body
}
```

is equivalent to this code in current Rust:

```rust
// `(a, b, c, ...)` is the list of all bindings in `pattern`.
let (a, b, c, ...) = match expression {
pattern => (a, b, c, ...),
_ => { body }
};
```

# Drawbacks

* “Must diverge” is an unusual requirement, which might be difficult to
explain or lead to confusing errors for programmers new to this feature.

* To a human scanning the code, it's not obvious when looking at the start of
a statement whether it is a `let ... else` or a regular `let` statement.

# Alternatives

* Don't make any changes; use existing syntax like `if let` and `match` as
shown above, or write macros to simplify the code.

* Use the same semantics with different syntax. For example, the original
version of this RFC used `if !let PAT = EXPR { BODY }`.

# Unresolved questions

* How much implementation complexity does this add?

[if-let]: https://github.com/rust-lang/rfcs/blob/master/text/0160-if-let.md
[swift]: https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/ControlFlow.html#//apple_ref/doc/uid/TP40014097-CH9-ID525