Refutable Let and Rust in 2018
Tags: rust, rust2018
Published: 2018-01-11 03:54:02

---
title: Refutable Let and Rust in 2018
published: true
description: Examining RFC 1303 Add a `let...else` expression 
tags: rust, rust2018
---

This examines the postponed [RFC #1303 "Add a `let...else` expression"][RFC#1303], addressing the RFC issue [#373 "Explicit refutable `let`][RFC#373].

This probably will end up formatted like an RFC, and I'd be willing to adapt it to a new RFC since the old one was postponed nearly two years ago now.

  [RFC#373]: <https://github.com/rust-lang/rfcs/issues/373>
  [RFC#1303]: <https://github.com/rust-lang/rfcs/pull/1303>
  [RFC#2221]: <https://github.com/rust-lang/rfcs/pull/2221>
  [RFC#2260]: <https://github.com/rust-lang/rfcs/pull/2260>
  [RUST#23416]: <https://github.com/rust-lang/rust/issues/23416>

## A motivating example

Consider a simple example:

```rust
if let Some(a) = make_a() {
    if let Some(b) = make_b(a) {
        if let Some(c) = make_c(b) {
            Ok(c)
        } else {
            Err("Failed to make C")?
        }
    } else {
        Err("Failed to make B")?
    }
} else {
    Err("Failed to make A")?
}
```

There are two key problems here. The first is locality of error handling. The error for line 1 is returned on line 12 of this tiny example. The second is rightward drift. The actual meat of the function (here just `Ok(c)`) is indented three levels deep. This RFC introduces a "refutable let binding" in order to address both of these issues.

First, let me address the obvious refactor to this minimal example. The current function signatures look something like the following:

```rust
fn make_a()     -> Option<A>;
fn make_b(a: A) -> Option<B>;
fn make_c(b: B) -> Option<C>;
```

The obvious "best" answer would be for these to return a `Result` with a descriptive error message that you could then propogate with `?`. In a real case, though, maybe `Option` really is the right semantic type to return, and maybe it's vendor code you can't change, etc. etc..

Secondly: You could `Option::ok_or_else?`. This is how I would write this today. To be fully honest, I'd maybe still write it that way, because `ok_or_else` is the exact behavior that I want and is very clear. But assume for the sake of argument that this is a more complicated example, such as destructuring a complicated ADT enum. I use `Option` here for simplicity.

The third option is to destructure using `match`:

```rust
let a = match make_a() {
    Some(a) => a,
    _ => Err("Failed to make A")?,
};
let b = match make_b(a) {
    Some(b) => b,
    _ => Err("Failed to make B")?,
};
let c = match make_c(b) {
    Some(c) => c,
    _ => Err("Failed to make C")?,
};
Ok(c)
```

or by "stuttering" an `if let`:

```rust
let a = if let Some(a) = make_a() {
    a
} else {
    Err("Failed to make A")?
};
let b = if let Some(b) = make_b(a) {
    b
} else {
    Err("Failed to make B")?
};
let c = if let Some(c) = make_c(b) {
    c
} else {
    Err("Failed to make C")?
};
Ok(c)
```

[playground](https://play.rust-lang.org/?gist=c0f017dc5a6d6e71b7c2feaae00c9ac0)

Here is the same example using a refutable let:

```rust
let Some(a) = make_a() else {
    Err("Failed to make A")?
};
let Some(b) = make_b(a) else {
    Err("Failed to make B")?
};
let Some(c) = make_c(b) else {
    Err("Failed to make C")?
};
Ok(c)
```

This is actually implementable using `macro_rules` macros, though not without some difficulty. [playground](https://play.rust-lang.org/?gist=1a50e5a509b310d3981db2e7b12fa453)

## Why?

The point of a refutable let is that you have some destructuring to do, and you want to handle the case where you cannot destructure by diverging from the function.

The simple desugar of `let...else` is the following transformation:

```rust
let PAT = EXPR else BLOCK;
// =>
let (bindings) = match EXPR {
    PAT => (bindings),
    _ => BLOCK: !,
}
```

where `BLOCK: !` is [type ascription][RUST#23416]. The ascribed `!` type makes it such that `BLOCK` needs to diverge (this effectively means `return`, `break` or `continue`). `(bindings)` here is a tuple of all assigned bindings in the pattern, so that they can be moved out of the pattern and into the containing scope.

You can hopefully see that this pattern reduces the required stuttering in using a `match` or a `let = if let` for this pattern of early-return to handle errors locally. More key, however, is that this _enforces the diverge_. If you write a `match` or `let = if let` or `unwrap_or_else` or anything other than a `?`, then the error handling branch could instead be a default value branch. Requiring the diverge at a language level increases the guarantees that a reader has when reading the code.

## Problems and alternate syntaxes

Consider parsing the following:

```rust
let foo = if bar { baz() } else { quux() };

// Option 1:
let foo = 
    (if bar { 
        baz()
    } else {
        quux()
    });

// Option 2:
let foo = (if bar { baz() })
else { quux() };
```

The second _is_ actually a vailid parse option if `baz()` is of type `()`. `if COND { () }` is valid in expr position and has type `()`. [playground](https://play.rust-lang.org/?gist=b7f691eec516d6f1bc893bebc184d3ca)

Unfortunately, this ambiguity means that `if PAT = EXPR else` is not a possible unambiguous syntax; there is a reason `else` isn't in the follow set for `expr`.

Other proposed syntaxes:

```rust
<keyword> let PAT = EXPR else BLOCK
```
Suffers from the same ambiguity.

```rust
if !let PAT = EXPR BLOCK
```
Not ambiguous, but suffers from overloading `if let`, which doesn't put bindings into the scope that contains it.

```rust
<keyword> let PAT = EXPR BLOCK
```
Unambiguous, but requires a new keyword. Keyword possibilities include `unless`.

## Recently

Two RFCs recently popped up that relate to this are [#2221 Guard Clause Flow Typing][RFC#2221] and [#2260 if- and while- let chains][RFC#2260].

The former is looking to get the same functionality from flow typing, and the author seems to not consider `let...else` as being a valid "guard". I'll not say more, lest I assert something that isn't true.

The latter looks to allow chaining `if let` to reduce nesting. Though this doesn't seem direclty related, it was mentioned often that having a refutable let would much reduce the necessity of being able to chain `if let` together at one block level.

## Looking forward to #Rust2018

Unless I've missed some glorious obvious-in-retrospect syntax, `if !let PAT = EXPR BLOCK` seems to be the only no-new-keyword-viable solution to the refutable let. Probably, this will end up being the syntax to use, and while maybe alien now, it might become as second nature as `if let` is now.

I can introduce a new RFC proposing using this syntax again, and detailing the problems with the other syntaxes. Maybe there the design can be fully, properly :bike: :shed: (you pick the emoji :slight_smile:).

For the rest of Rust in 2018, I agree with much of the other #Rust2018 posts. Rust would probably be best served by a tick/tock cycle, where this year is spent on paying down stabilization debt. Many great features are just around the corner and just need that final push on design/implementation work.

This is not to say nothing new should happen; WebAssembly work should continue, the RFC machine should continue moving (though we shouldn't tell people to go submit ideas as rapidly as happened before the impl period cutoff). Stabilizing key tools, landing the impl period features, and (re)clarifying stability. Some way to endorse crates as high quality, ready-for-production libraries. Async/await/generators.

These are all _great_ things which have been started. Let's move them towards finished together!

Oh, and my never-gonna-happen breakage whishlist: make all of the `Iterator` fn return `impl Iterator` instead of concrete types. `impl Trait` all of the things.

UPDATE to above: u/QuietMisdreavus [pointed out the flaw with this](https://www.reddit.com/r/rust/comments/7plf8z/examining_rfc_1303_and_rust2018/dsj5lnc/):

> This is actually a loss of information in many cases. Several of the `Iterator` wrappers also conditionally implement `DoubleEndedIterator`, `ExactSizeIterator`, `FusedIterator`, etc, based on their contained type. As far as i know, there's no way to represent that in `impl Trait` syntax.

Having realized the error of my ways, this is no longer a choice I would make.