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

RFC: let-else statements #3137

Merged
merged 25 commits into from
Jul 21, 2021
Merged

RFC: let-else statements #3137

merged 25 commits into from
Jul 21, 2021

Conversation

Fishrock123
Copy link
Contributor

@Fishrock123 Fishrock123 commented Jun 3, 2021

Tracking issue: rust-lang/rust#87335

Introduce a new let PATTERN: TYPE = EXPRESSION else DIVERGING_BLOCK; construct (informally called a
let-else statement), the counterpart of if-let expressions.

If the pattern match from the assigned expression succeeds, its bindings are introduced into the
surrounding scope
. If it does not succeed, it must diverge (return !, e.g. return or break).
Technically speaking, let-else statements are refutable let statements.
The expression has some restrictions, notably it may not be an ExpressionWithBlock or LazyBooleanExpression.

This RFC is a modernization of a 2015 RFC (pull request 1303) for an almost identical feature.

Rendered


Thanks to all the folks on Zulip who were willing to discuss this proposal once again and helped me draft this RFC.
In particular: Josh Triplett, scottmcm, Mario Carneiro, Frank Steffahn, Dirkjan Ochtman, and bstrie.

Zulip topic: https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/topic/.60let.20pattern.20.3D.20expr.20else.20.7B.20.2E.2E.2E.20.7D.60.20statements

Specifically bstrie's suggestion of `ExpressionWithoutBlock`.

Also some spelling fixes and example updates.
Thanks to Frank Steffahn for pointing this out in Zulip.
By request of Josh Triplett
Also mentions the diverging return type of `!` in the summary.
text/0000-let-else.md Outdated Show resolved Hide resolved
text/0000-let-else.md Outdated Show resolved Hide resolved
text/0000-let-else.md Outdated Show resolved Hide resolved
text/0000-let-else.md Outdated Show resolved Hide resolved
text/0000-let-else.md Outdated Show resolved Hide resolved
text/0000-let-else.md Outdated Show resolved Hide resolved
text/0000-let-else.md Outdated Show resolved Hide resolved
text/0000-let-else.md Outdated Show resolved Hide resolved
text/0000-let-else.md Outdated Show resolved Hide resolved
text/0000-let-else.md Outdated Show resolved Hide resolved
text/0000-let-else.md Outdated Show resolved Hide resolved
Co-Authored-By: Josh Triplett <josh@joshtriplett.org>
@burdges
Copy link

burdges commented Jun 3, 2021

It's be more readable if else were replaced by otherwise, or really anything besides else since rust lacks then.

@lebensterben
Copy link

lebensterben commented Jun 3, 2021

The example

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");
}

can be written as

match (x, y, z) {
  ((Some(a), Some(b), Some(c)) => do_something_with(a, b, c),
  ((Some(a), Some(b), _)) => return Err("bad z"),
  ((Some(a), _ , _)) => return Err("bad y"),
  _ => return Err("bad x"),
}

which doesn't seem bad comparing to the proposed

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);

The real-world example can just use if-let and I don't understand

consider how the following code would look without let-else

@burdges
Copy link

burdges commented Jun 4, 2021

You meant

match (x, y, z) {
  ((Some(a), Some(b), Some(c)) => do_something_with(a, b, c),
  ((_, _, None)) => return Err("bad z"),
  ((_, None, _)) => return Err("bad y"),
  _ => return Err("bad x"),
}

but simpler competition exists, like

let a = if let Some(a) = x { a } else { return Err("bad x"); };
...

If you're doing anything with Result conversions like Option then idiomatic Rust code goes

let a = x.ok_or("bad x) ?;
let b = y.ok_or("bad y) ?;
let c = z.ok_or("bad z) ?;
do_something_with(a, b, c);

or

do_something_with(
    x.ok_or("bad x) ?,
    y.ok_or("bad y) ?,
    z.ok_or("bad z) ?
)

I'd say remove Option entirely from the RFC to avoid confusion. Another enum like Cow works better, maybe combined with Vec::reserve instead of returns because otherwise

let owned_or = |moo,err| if moo.is_owned() { Ok(moo.into_owned()) } else { Err(err) };
do_something_with(
    x.owned_or("bad x) ?,
    y.owned_or("bad y) ?,
    z.owned_or("bad z) ?
)

In reality, this shortcut would exist exclusively for custom enums and should probably never be used with anything from std.

@lebensterben
Copy link

@burdges
let a = if let Some(a) = x { a } else { return Err("bad x"); }; is not simpler.
When it's properly formatted it takes more lines than mine example, which is bad if concise source code is an objective.

Option doesn't cause any particular confusion to me. What I'm confused is why the proposed let-else is considered better than if-let and match.

If there's one refutable pattern to match, then if-let is good enough and let-else adds no utility to it.
If there are multiple refutable patterns to match, just use match and you will get concise and readable code.

@Fishrock123
Copy link
Contributor Author

Fishrock123 commented Jun 4, 2021

match (x, y, z) {
  ((Some(a), Some(b), Some(c)) => do_something_with(a, b, c),
  ((Some(a), Some(b), _)) => return Err("bad z"),
  ((Some(a), _ , _)) => return Err("bad y"),
  _ => return Err("bad x"),
}

That basic example may need to be improved a bit. Often times in practical use there is other code between the previous and next if statement, which cannot be represented by this match example. Keep in mind no example can perfectly represent all the possibilities in real-world code, which is why there are multiple examples.

If there's one refutable pattern to match, then if-let is good enough and let-else adds no utility to it.

It can in fact add practical utility. Please see the practical refactor section of the RFC.

If you're doing anything with Result conversions like Option then idiomatic Rust code goes

This is already covered in the RFC in more detail. See the alternatives section which notes that ok_or/ok_or_else + ? is great for Result/Option but untenable for other enums, particularly non-stdlib enums.

@lebensterben
Copy link

Also consider the following case

enum Fruit {
    Apple(u32),
    Banana(u32),
}

struct AppleJuice;

fn make_apple_juice(fruit: &Fruit) -> Result<AppleJuice, String> {
    match fruit {
        Fruit::Apple(v) => Ok(AppleJuice),
        Fruit::Banana(v) => Err(String::from("Require apple, found %s bananas", v)),
    }
}

This cannot be written with the proposed let-else construct or if-let construct.
So usage of let-else is really limited.

@Fishrock123
Copy link
Contributor Author

Fishrock123 commented Jun 4, 2021

enum Fruit {
    Apple(u32),
    Banana(u32),
}

Enums with multiple value-bearing variants do not necessarily need or want other variant values to be accessed in case of a failed match. The examples section does cover this. Also if it helps, the GeoJson enum used in the one example is this: https://docs.rs/geojson/0.22.2/geojson/enum.GeoJson.html

@lebensterben
Copy link

lebensterben commented Jun 4, 2021

@Fishrock123

It can in fact add practical utility. Please see the practical refactor section of the RFC.

This refactor is not a fair comparison.
How's

let GeoJson::FeatureCollection(features) = geojson else {
    return Err(format_err_status!(
        422,
        "GeoJSON was not a Feature Collection",
    ));
};

any better than

let features = if let GeoJson::FeatureCollection(features) = geojson {
    features
} else {
    return Err(format_err_status!(
        422,
        "GeoJSON was not a Feature Collection",
    ));
};

It merely removed two lines. Does this justify adding the new constuct let-else?

@Fishrock123
Copy link
Contributor Author

Fishrock123 commented Jun 4, 2021

Does this justify adding the new constuct let-else?

let x = if let Pattern(x) = y { x } else {
    // return or something
};

Any real-world code I've seen uses match over the if let such as the above. The if let like this reads especially poor.
It would be better if people were not likely to use the same variable name in the intermediate binding, but all examples that exist seem to do that.

That being said, yes I do think the few lines of elimination bring enhanced clarity and newcomer discoverability, especially for something which can be pretty common, and which desugars into something pretty simple without much complexity.

Quoting myself from the RFC:

While that refactor was positive, it should be noted that such alternatives were unclear the authors when they were less experienced rust programmers, and also that the resulting match code includes syntax boilerplate (e.g. the block) that could theoretically be reduced today but also interferes with rustfmt's rules:

@joshtriplett
Copy link
Member

@lebensterben wrote:

can be written as

match (x, y, z) {
  ((Some(a), Some(b), Some(c)) => do_something_with(a, b, c),
  ((Some(a), Some(b), _)) => return Err("bad z"),
  ((Some(a), _ , _)) => return Err("bad y"),
  _ => return Err("bad x"),
}

This is assuming that the actual expressions involved in y and z (which are examples) don't involve any previous bindings. If y is actually f(x) this doesn't work. And that's a common pattern: refutably bind x or fail, then refutably bind y or fail...

@nielsle
Copy link

nielsle commented Jun 4, 2021

The best use case for let-else is probably something like the following (where foo and bar are functions with side effects)

let Some(a) = foo() else {
    log("Bad foo");
    return Err("foo failed");
};

let Some(b) = bar() else {
    log("Bad bar");
    return Err("bar failed");
};

This example can also be rewritten using if-else or map_err()? or two separate match statements, but admittedly the version with let-else is slightly prettier.

@burdges
Copy link

burdges commented Jun 4, 2021

Does this justify adding the new constuct let-else?

I think only the word else adds confusion here. If another word gets used, ala let x = y otherwise { z };, then one conceptualizes this roughly like let x = y? ala let x = y otherwise { return None; }; or whatever.

@Pzixel
Copy link

Pzixel commented Aug 23, 2021

If the pattern match from the assigned expression succeeds, its bindings are introduced into the
surrounding scope. If it does not succeed, it must diverge (return !, e.g. return or break).

Should this be the case? From C# experience we know that it may be used like this:

if (new object() is not int x) {
	x = 10;
}
Console.WriteLine(x); // prints 10

So if value is NOT matched then if block gets executed.

I think that it coul be applied here as well: instead of returning/breaking/... it may actually set the variable. You may do this with unwrap_or_else(..) combinator but sometimes it's quite awkward to use.

@mqudsi
Copy link

mqudsi commented Aug 23, 2021

The C# sample is closer to a match block (indeed, that's referred to as pattern matching in C#).

if (new object() is not int x) {
	x = 10;
}
Console.WriteLine(x); // prints 10

is equivalent to rust's

let x = match Object::new() {
    Some(x) => x,
    None => 10,
}

(Except it's not an Option<T> but rather an unboxing operation which isn't something rust supports.)

The important bit is that commenting out x = 10; will result in a hard compile-time error ("Use of unassigned local variable x").

@kennytm kennytm mentioned this pull request Aug 23, 2021
@Pzixel
Copy link

Pzixel commented Aug 23, 2021

Nope, it's not the equivalent. Your rust code is equivalent to

var x = (new object()) switch {
  int x => x,
  _ => 10
};

But it's not what I've written. This code creates a scope (and indentation) which is what we're trying to avoid here. Consider that instead of Console.WriteLine there are dozens of lines. Instead of linear happy path with guardian checks you get a huge nested pile of code which isn't pleasant to read.

The important bit is that commenting out x = 10; will result in a hard compile-time error ("Use of unassigned local variable x").

Right, this is what I meant, but I missed this, thanks. Rust shouldn't require block do diverge but it should treat binding as unassigned instead (because well it is), and that's it

@mqudsi
Copy link

mqudsi commented Aug 23, 2021

@Pzixel I wasn't disagreeing with you and I wasn't posting an equivalent developer UX but rather an equivalent compilation to clarify the behavior for those not familiar with C#. Especially the bit about C# not letting you just escape the typeguard willy-nilly, for those that don't/didn't realize how strictly typed C# was in that regard.

@digama0
Copy link
Contributor

digama0 commented Aug 24, 2021

The approach with x = 10 in the body was discussed and rejected earlier in the zulip thread. You can't just use 10 in the body because this gets messy when there are multiple variables being bound in the pattern, and while x = 10 works, it is misleading in the very common case that the newly introduced variable shadows another one:

let x: Result<usize, usize> = Err(2);
let Ok(x) = x else {
    x = x.unwrap_err(); // error: using uninitialized variable `x: usize`
};

@Pzixel
Copy link

Pzixel commented Aug 24, 2021

I'm not sure how is it misleading. It works just in the same way:

if (default(int[]) is not { Length: 5, LongLength: var y } x)
{
	x = new int[] { 10 };
	y = 1;
}
Console.WriteLine(x);
Console.WriteLine(y);

Is variable was not set it's just unbinded and causes CS0165 Use of unassigned local variable if you're trying to use it. It makes total sense to me.

In your example you won't be able to use the shadowed x because, well, it's shadowed but I don't see this as a problem. This approaches allows some use cases that current approach doesn't but not the other way around. This code wouldn't be valid with current specification as well. But approach with unitialized variables allows some subset of operations that is forbidden with "should diverge" approach

@teohhanhui
Copy link

teohhanhui commented Aug 24, 2021

What does this do that if let ... else doesn't provide?

EDIT: Uhh, okay, it doesn't create another level of nesting.

@digama0
Copy link
Contributor

digama0 commented Aug 24, 2021

In your example you won't be able to use the shadowed x because, well, it's shadowed but I don't see this as a problem. This approaches allows some use cases that current approach doesn't but not the other way around. This code wouldn't be valid with current specification as well. But approach with unitialized variables allows some subset of operations that is forbidden with "should diverge" approach

What is misleading is that there are two somewhat reasonable options for the binding structure, and people will have different predispositions to one or the other, and if they stumble on some code like this without having read the RFC carefully they might be mislead. Personally, I am predisposed to expect that a variable should not be in scope in a different branch from where it is introduced - nothing else in rust is like that - so in let Some(x) = ... else { <here> }; I would not expect x to be in scope. It isn't in scope in if let which is the nearest other grammar construction in rust, so even for people who have come to learn the rules it will feel inconsistent.

The idea behind forced divergence here is that you don't have to make a choice - you don't need to know what happens after the block completes, where it goes and what values are assigned - because it never gets there and we ensure this is always the case.

The situation with using the shadowed variable seems rather likely to me, for example:

let Foo::Bar(x) = x else { panic!("unexpected: {:?}", x) };

I'm using only rust examples here, not C#, because I think the intuitions might differ, since unboxing has a significantly different syntactic expression, and I think shadowing is quite a bit less common. (Actually I just tested, and variable shadowing in the direct sense of int x = 1; int x = 2; is outright banned, and there are more complex shadowing situations that are allowed but strongly discouraged. So indeed the intuitions will be very different coming from a C# background.)

@Pzixel
Copy link

Pzixel commented Aug 25, 2021

I'm using only rust examples here, not C#, because I think the intuitions might differ, since unboxing has a significantly different syntactic expression, and I think shadowing is quite a bit less common.

C# has (un)boxing and stuff, right, but this is beside the point. What I'm saying that assigning variable that wasn't bind may be useful. Of course if you want to do so you need to assign this to another name otherwise the old one would be shadowed.

I think both approaches has pros and cons, and I'm sharing my usage examples to help making proper decision.

(Actually I just tested, and variable shadowing in the direct sense of int x = 1; int x = 2; is outright banned, and there are more complex shadowing situations that are allowed but strongly discouraged. So indeed the intuitions will be very different coming from a C# background.)

This is also true but also beside the point: C# doesn't have shadowing of locals but we still can imagine how would it work in Rust. For example:

let Foo::Bar(x) = x else { panic!("unexpected: {:?}", x) }; // is not allowed: x is not assigned
let Foo::Bar(y) = x else { panic!("unexpected: {:?}", x) }; // ok
let Foo::Bar(y) = x else { y = 15 }; // ok

@Fishrock123
Copy link
Contributor Author

Fishrock123 commented Aug 25, 2021

Please see the fallback assignment part of the RFC discussed under rationale and also mentioned in future possibilities:
https://github.com/Fishrock123/rfcs/blob/let-else/text/0000-let-else.md#else-block-fall-back-assignment &
https://github.com/Fishrock123/rfcs/blob/let-else/text/0000-let-else.md#fall-back-assignment

yvt added a commit to yvt/interlock-rs that referenced this pull request Sep 5, 2021
Replaces `guard::guard!` with the native `if let ... else` from
<rust-lang/rfcs#3137> (still unstable).
GuillaumeGomez added a commit to GuillaumeGomez/rust that referenced this pull request Sep 16, 2022
…plett

Stabilize `let else`

:tada:  **Stabilizes the `let else` feature, added by [RFC 3137](rust-lang/rfcs#3137 🎉

Reference PR: rust-lang/reference#1156

closes rust-lang#87335 (`let else` tracking issue)

FCP: rust-lang#93628 (comment)

----------

## Stabilization report

### Summary

The feature allows refutable patterns in `let` statements if the expression is
followed by a diverging `else`:

```Rust
fn get_count_item(s: &str) -> (u64, &str) {
    let mut it = s.split(' ');
    let (Some(count_str), Some(item)) = (it.next(), it.next()) else {
        panic!("Can't segment count item pair: '{s}'");
    };
    let Ok(count) = u64::from_str(count_str) else {
        panic!("Can't parse integer: '{count_str}'");
    };
    (count, item)
}
assert_eq!(get_count_item("3 chairs"), (3, "chairs"));
```

### Differences from the RFC / Desugaring

Outside of desugaring I'm not aware of any differences between the implementation and the RFC. The chosen desugaring has been changed from the RFC's [original](https://rust-lang.github.io/rfcs/3137-let-else.html#reference-level-explanations). You can read a detailed discussion of the implementation history of it in `@cormacrelf` 's [summary](rust-lang#93628 (comment)) in this thread, as well as the [followup](rust-lang#93628 (comment)). Since that followup, further changes have happened to the desugaring, in rust-lang#98574, rust-lang#99518, rust-lang#99954. The later changes were mostly about the drop order: On match, temporaries drop in the same order as they would for a `let` declaration. On mismatch, temporaries drop before the `else` block.

### Test cases

In chronological order as they were merged.

Added by df9a2e0 (rust-lang#87688):

* [`ui/pattern/usefulness/top-level-alternation.rs`](https://github.com/rust-lang/rust/blob/1.58.1/src/test/ui/pattern/usefulness/top-level-alternation.rs) to ensure the unreachable pattern lint visits patterns inside `let else`.

Added by 5b95df4 (rust-lang#87688):

* [`ui/let-else/let-else-bool-binop-init.rs`](https://github.com/rust-lang/rust/blob/1.58.1/src/test/ui/let-else/let-else-bool-binop-init.rs) to ensure that no lazy boolean expressions (using `&&` or `||`) are allowed in the expression, as the RFC mandates.
* [`ui/let-else/let-else-brace-before-else.rs`](https://github.com/rust-lang/rust/blob/1.58.1/src/test/ui/let-else/let-else-brace-before-else.rs) to ensure that no `}` directly preceding the `else` is allowed in the expression, as the RFC mandates.
* [`ui/let-else/let-else-check.rs`](https://github.com/rust-lang/rust/blob/1.58.1/src/test/ui/let-else/let-else-check.rs) to ensure that `#[allow(...)]` attributes added to the entire `let` statement apply for the `else` block.
* [`ui/let-else/let-else-irrefutable.rs`](https://github.com/rust-lang/rust/blob/1.58.1/src/test/ui/let-else/let-else-irrefutable.rs) to ensure that the `irrefutable_let_patterns` lint fires.
* [`ui/let-else/let-else-missing-semicolon.rs`](https://github.com/rust-lang/rust/blob/1.58.1/src/test/ui/let-else/let-else-missing-semicolon.rs) to ensure the presence of semicolons at the end of the `let` statement.
* [`ui/let-else/let-else-non-diverging.rs`](https://github.com/rust-lang/rust/blob/1.58.1/src/test/ui/let-else/let-else-non-diverging.rs) to ensure the `else` block diverges.
* [`ui/let-else/let-else-run-pass.rs`](https://github.com/rust-lang/rust/blob/1.58.1/src/test/ui/let-else/let-else-run-pass.rs) to ensure the feature works in some simple test case settings.
* [`ui/let-else/let-else-scope.rs`](https://github.com/rust-lang/rust/blob/1.58.1/src/test/ui/let-else/let-else-scope.rs) to ensure the bindings created by the outer `let` expression are not available in the `else` block of it.

Added by bf7c32a (rust-lang#89965):

* [`ui/let-else/issue-89960.rs`](https://github.com/rust-lang/rust/blob/1.58.1/src/test/ui/let-else/issue-89960.rs) as a regression test for the ICE-on-error bug rust-lang#89960 . Later in 102b912 this got removed in favour of more comprehensive tests.

Added by 8565419 (rust-lang#89974):

* [`ui/let-else/let-else-if.rs`](https://github.com/rust-lang/rust/blob/1.58.1/src/test/ui/let-else/let-else-if.rs) to test for the improved error message that points out that `let else if` is not possible.

Added by 9b45713:

* [`ui/let-else/let-else-allow-unused.rs`](https://github.com/rust-lang/rust/blob/1.61.0/src/test/ui/let-else/let-else-allow-unused.rs) as a regression test for rust-lang#89807, to ensure that `#[allow(...)]` attributes added to the entire `let` statement apply for bindings created by the `let else` pattern.

Added by 61bcd8d (rust-lang#89841):

* [`ui/let-else/let-else-non-copy.rs`](https://github.com/rust-lang/rust/blob/1.61.0/src/test/ui/let-else/let-else-non-copy.rs) to ensure that a copy is performed out of non-copy wrapper types. This mirrors `if let` behaviour. The test case bases on rustc internal changes originally meant for rust-lang#89933 but then removed from the PR due to the error prior to the improvements of rust-lang#89841.
* [`ui/let-else/let-else-source-expr-nomove-pass.rs `](https://github.com/rust-lang/rust/blob/1.61.0/src/test/ui/let-else/let-else-source-expr-nomove-pass.rs) to ensure that while there is a move of the binding in the successful case, the `else` case can still access the non-matching value. This mirrors `if let` behaviour.

Added by 102b912 (rust-lang#89841):

* [`ui/let-else/let-else-ref-bindings.rs`](https://github.com/rust-lang/rust/blob/1.61.0/src/test/ui/let-else/let-else-ref-bindings.rs) and [`ui/let-else/let-else-ref-bindings-pass.rs `](https://github.com/rust-lang/rust/blob/1.61.0/src/test/ui/let-else/let-else-ref-bindings-pass.rs) to check `ref` and `ref mut` keywords in the pattern work correctly and error when needed.

Added by 2715c5f (rust-lang#89841):

* Match ergonomic tests adapted from the `rfc2005` test suite.

Added by fec8a50 (rust-lang#89841):

* [`ui/let-else/let-else-deref-coercion-annotated.rs`](https://github.com/rust-lang/rust/blob/1.61.0/src/test/ui/let-else/let-else-deref-coercion-annotated.rs) and [`ui/let-else/let-else-deref-coercion.rs`](https://github.com/rust-lang/rust/blob/1.61.0/src/test/ui/let-else/let-else-deref-coercion.rs) to check deref coercions.

#### Added since this stabilization report was originally written (2022-02-09)

Added by 76ea566 (rust-lang#94211):

* [`ui/let-else/let-else-destructuring.rs`](https://github.com/rust-lang/rust/blob/1.63.0/src/test/ui/let-else/let-else-destructuring.rs) to give a nice error message if an user tries to do an assignment with a (possibly refutable) pattern and an `else` block, like asked for in rust-lang#93995.

Added by e7730dc (rust-lang#94208):

* [`ui/let-else/let-else-allow-in-expr.rs`](https://github.com/rust-lang/rust/blob/1.61.0/src/test/ui/let-else/let-else-allow-in-expr.rs) to test whether `#[allow(unused_variables)]` works in the expr, as well as its non presence, as well as putting it on the entire `let else` *affects* the expr, too. This was adding a missing test as pointed out by the stabilization report.
* Expansion of `ui/let-else/let-else-allow-unused.rs` and `ui/let-else/let-else-check.rs` to ensure that non-presence of `#[allow(unused)]` does issue the unused lint. This was adding a missing test case as pointed out by the stabilization report.

Added by 5bd7106 (rust-lang#94208):

* [`ui/let-else/let-else-slicing-error.rs`](https://github.com/rust-lang/rust/blob/1.61.0/src/test/ui/let-else/let-else-slicing-error.rs), a regression test for rust-lang#92069, which got fixed without addition of a regression test. This resolves a missing test as pointed out by the stabilization report.

Added by 5374688 (rust-lang#98574):

* [`src/test/ui/async-await/async-await-let-else.rs`](https://github.com/rust-lang/rust/blob/master/src/test/ui/async-await/async-await-let-else.rs) to test the interaction of async/await with `let else`

Added by 6c529de (rust-lang#98574):

* [`src/test/ui/let-else/let-else-temporary-lifetime.rs`](https://github.com/rust-lang/rust/blob/master/src/test/ui/let-else/let-else-temporary-lifetime.rs) as a (partial) regression test for rust-lang#98672

Added by 9b56640 (rust-lang#99518):

* [`src/test/ui/let-else/let-else-temp-borrowck.rs`](https://github.com/rust-lang/rust/blob/master/src/test/ui/let-else/let-else-temporary-lifetime.rs) as a regression test for rust-lang#93951
* Extension of `src/test/ui/let-else/let-else-temporary-lifetime.rs` to include a partial regression test for rust-lang#98672 (especially regarding `else` drop order)

Added by baf9a7c (rust-lang#99518):

* Extension of `src/test/ui/let-else/let-else-temporary-lifetime.rs` to include a partial regression test for rust-lang#93951, similar to `let-else-temp-borrowck.rs`

Added by 60be2de (rust-lang#99518):

* Extension of `src/test/ui/let-else/let-else-temporary-lifetime.rs` to include a program that can now be compiled thanks to borrow checker implications of rust-lang#99518

Added by 47a7a91 (rust-lang#100132):

* [`src/test/ui/let-else/issue-100103.rs`](https://github.com/rust-lang/rust/blob/master/src/test/ui/let-else/issue-100103.rs), as a regression test for rust-lang#100103, to ensure that there is no ICE when doing `Err(...)?` inside else blocks.

Added by e3c5bd6 (rust-lang#100443):

* [`src/test/ui/let-else/let-else-then-diverge.rs`](https://github.com/rust-lang/rust/blob/master/src/test/ui/let-else/let-else-then-diverge.rs), to verify that there is no unreachable code error with the current desugaring.

Added by 9818526 (rust-lang#100443):

* [`src/test/ui/let-else/issue-94176.rs`](https://github.com/rust-lang/rust/blob/master/src/test/ui/let-else/issue-94176.rs), to make sure that a correct span is emitted for a missing trailing expression error. Regression test for rust-lang#94176.

Added by e182d12 (rust-lang#100434):

* [src/test/ui/unpretty/pretty-let-else.rs](https://github.com/rust-lang/rust/blob/master/src/test/ui/unpretty/pretty-let-else.rs), as a regression test to ensure pretty printing works for `let else` (this bug surfaced in many different ways)

Added by e262856 (rust-lang#99954):

* [`src/test/ui/let-else/let-else-temporary-lifetime.rs`](https://github.com/rust-lang/rust/blob/master/src/test/ui/let-else/let-else-temporary-lifetime.rs) extended to contain & borrows as well, as this was identified as an earlier issue with the desugaring: rust-lang#98672 (comment)

Added by 2d8460e (rust-lang#99291):

* [`src/test/ui/let-else/let-else-drop-order.rs`](https://github.com/rust-lang/rust/blob/master/src/test/ui/let-else/let-else-drop-order.rs) a matrix based test for various drop order behaviour of `let else`. Especially, it verifies equality of `let` and `let else` drop orders, [resolving](rust-lang#93628 (comment)) a [stabilization blocker](rust-lang#93628 (comment)).

Added by 1b87ce0 (rust-lang#101410):

* Edit to `src/test/ui/let-else/let-else-temporary-lifetime.rs` to add the `-Zvalidate-mir` flag, as a regression test for rust-lang#99228

Added by af591eb (rust-lang#101410):

* [`src/test/ui/let-else/issue-99975.rs`](https://github.com/rust-lang/rust/blob/master/src/test/ui/let-else/issue-99975.rs) as a regression test for the ICE rust-lang#99975.

Added by this PR:

* `ui/let-else/let-else.rs`, a simple run-pass check, similar to `ui/let-else/let-else-run-pass.rs`.

### Things not currently tested

* ~~The `#[allow(...)]` tests check whether allow works, but they don't check whether the non-presence of allow causes a lint to fire.~~ → *test added by e7730dc*
* ~~There is no `#[allow(...)]` test for the expression, as there are tests for the pattern and the else block.~~ → *test added by e7730dc*
* ~~`let-else-brace-before-else.rs` forbids the `let ... = {} else {}` pattern and there is a rustfix to obtain `let ... = ({}) else {}`. I'm not sure whether the `.fixed` files are checked by the tooling that they compile. But if there is no such check, it would be neat to make sure that `let ... = ({}) else {}` compiles.~~ → *test added by e7730dc*
* ~~rust-lang#92069 got closed as fixed, but no regression test was added. Not sure it's worth to add one.~~ → *test added by 5bd7106*
* ~~consistency between `let else` and `if let` regarding lifetimes and drop order: rust-lang#93628 (comment) → *test added by 2d8460e*

Edit: they are all tested now.

### Possible future work / Refutable destructuring assignments

[RFC 2909](https://rust-lang.github.io/rfcs/2909-destructuring-assignment.html) specifies destructuring assignment, allowing statements like `FooBar { a, b, c } = foo();`.
As it was stabilized, destructuring assignment only allows *irrefutable* patterns, which before the advent of `let else` were the only patterns that `let` supported.
So the combination of `let else` and destructuring assignments gives reason to think about extensions of the destructuring assignments feature that allow refutable patterns, discussed in rust-lang#93995.

A naive mapping of `let else` to destructuring assignments in the form of `Some(v) = foo() else { ... };` might not be the ideal way. `let else` needs a diverging `else` clause as it introduces new bindings, while assignments have a default behaviour to fall back to if the pattern does not match, in the form of not performing the assignment. Thus, there is no good case to require divergence, or even an `else` clause at all, beyond the need for having *some* introducer syntax so that it is clear to readers that the assignment is not a given (enums and structs look similar). There are better candidates for introducer syntax however than an empty `else {}` clause, like `maybe` which could be added as a keyword on an edition boundary:

```Rust
let mut v = 0;
maybe Some(v) = foo(&v);
maybe Some(v) = foo(&v) else { bar() };
```

Further design discussion is left to an RFC, or the linked issue.
Dylan-DPC added a commit to Dylan-DPC/rust that referenced this pull request Sep 17, 2022
…plett

Stabilize `let else`

:tada:  **Stabilizes the `let else` feature, added by [RFC 3137](rust-lang/rfcs#3137 🎉

Reference PR: rust-lang/reference#1156

closes rust-lang#87335 (`let else` tracking issue)

FCP: rust-lang#93628 (comment)

----------

## Stabilization report

### Summary

The feature allows refutable patterns in `let` statements if the expression is
followed by a diverging `else`:

```Rust
fn get_count_item(s: &str) -> (u64, &str) {
    let mut it = s.split(' ');
    let (Some(count_str), Some(item)) = (it.next(), it.next()) else {
        panic!("Can't segment count item pair: '{s}'");
    };
    let Ok(count) = u64::from_str(count_str) else {
        panic!("Can't parse integer: '{count_str}'");
    };
    (count, item)
}
assert_eq!(get_count_item("3 chairs"), (3, "chairs"));
```

### Differences from the RFC / Desugaring

Outside of desugaring I'm not aware of any differences between the implementation and the RFC. The chosen desugaring has been changed from the RFC's [original](https://rust-lang.github.io/rfcs/3137-let-else.html#reference-level-explanations). You can read a detailed discussion of the implementation history of it in `@cormacrelf` 's [summary](rust-lang#93628 (comment)) in this thread, as well as the [followup](rust-lang#93628 (comment)). Since that followup, further changes have happened to the desugaring, in rust-lang#98574, rust-lang#99518, rust-lang#99954. The later changes were mostly about the drop order: On match, temporaries drop in the same order as they would for a `let` declaration. On mismatch, temporaries drop before the `else` block.

### Test cases

In chronological order as they were merged.

Added by df9a2e0 (rust-lang#87688):

* [`ui/pattern/usefulness/top-level-alternation.rs`](https://github.com/rust-lang/rust/blob/1.58.1/src/test/ui/pattern/usefulness/top-level-alternation.rs) to ensure the unreachable pattern lint visits patterns inside `let else`.

Added by 5b95df4 (rust-lang#87688):

* [`ui/let-else/let-else-bool-binop-init.rs`](https://github.com/rust-lang/rust/blob/1.58.1/src/test/ui/let-else/let-else-bool-binop-init.rs) to ensure that no lazy boolean expressions (using `&&` or `||`) are allowed in the expression, as the RFC mandates.
* [`ui/let-else/let-else-brace-before-else.rs`](https://github.com/rust-lang/rust/blob/1.58.1/src/test/ui/let-else/let-else-brace-before-else.rs) to ensure that no `}` directly preceding the `else` is allowed in the expression, as the RFC mandates.
* [`ui/let-else/let-else-check.rs`](https://github.com/rust-lang/rust/blob/1.58.1/src/test/ui/let-else/let-else-check.rs) to ensure that `#[allow(...)]` attributes added to the entire `let` statement apply for the `else` block.
* [`ui/let-else/let-else-irrefutable.rs`](https://github.com/rust-lang/rust/blob/1.58.1/src/test/ui/let-else/let-else-irrefutable.rs) to ensure that the `irrefutable_let_patterns` lint fires.
* [`ui/let-else/let-else-missing-semicolon.rs`](https://github.com/rust-lang/rust/blob/1.58.1/src/test/ui/let-else/let-else-missing-semicolon.rs) to ensure the presence of semicolons at the end of the `let` statement.
* [`ui/let-else/let-else-non-diverging.rs`](https://github.com/rust-lang/rust/blob/1.58.1/src/test/ui/let-else/let-else-non-diverging.rs) to ensure the `else` block diverges.
* [`ui/let-else/let-else-run-pass.rs`](https://github.com/rust-lang/rust/blob/1.58.1/src/test/ui/let-else/let-else-run-pass.rs) to ensure the feature works in some simple test case settings.
* [`ui/let-else/let-else-scope.rs`](https://github.com/rust-lang/rust/blob/1.58.1/src/test/ui/let-else/let-else-scope.rs) to ensure the bindings created by the outer `let` expression are not available in the `else` block of it.

Added by bf7c32a (rust-lang#89965):

* [`ui/let-else/issue-89960.rs`](https://github.com/rust-lang/rust/blob/1.58.1/src/test/ui/let-else/issue-89960.rs) as a regression test for the ICE-on-error bug rust-lang#89960 . Later in 102b912 this got removed in favour of more comprehensive tests.

Added by 8565419 (rust-lang#89974):

* [`ui/let-else/let-else-if.rs`](https://github.com/rust-lang/rust/blob/1.58.1/src/test/ui/let-else/let-else-if.rs) to test for the improved error message that points out that `let else if` is not possible.

Added by 9b45713:

* [`ui/let-else/let-else-allow-unused.rs`](https://github.com/rust-lang/rust/blob/1.61.0/src/test/ui/let-else/let-else-allow-unused.rs) as a regression test for rust-lang#89807, to ensure that `#[allow(...)]` attributes added to the entire `let` statement apply for bindings created by the `let else` pattern.

Added by 61bcd8d (rust-lang#89841):

* [`ui/let-else/let-else-non-copy.rs`](https://github.com/rust-lang/rust/blob/1.61.0/src/test/ui/let-else/let-else-non-copy.rs) to ensure that a copy is performed out of non-copy wrapper types. This mirrors `if let` behaviour. The test case bases on rustc internal changes originally meant for rust-lang#89933 but then removed from the PR due to the error prior to the improvements of rust-lang#89841.
* [`ui/let-else/let-else-source-expr-nomove-pass.rs `](https://github.com/rust-lang/rust/blob/1.61.0/src/test/ui/let-else/let-else-source-expr-nomove-pass.rs) to ensure that while there is a move of the binding in the successful case, the `else` case can still access the non-matching value. This mirrors `if let` behaviour.

Added by 102b912 (rust-lang#89841):

* [`ui/let-else/let-else-ref-bindings.rs`](https://github.com/rust-lang/rust/blob/1.61.0/src/test/ui/let-else/let-else-ref-bindings.rs) and [`ui/let-else/let-else-ref-bindings-pass.rs `](https://github.com/rust-lang/rust/blob/1.61.0/src/test/ui/let-else/let-else-ref-bindings-pass.rs) to check `ref` and `ref mut` keywords in the pattern work correctly and error when needed.

Added by 2715c5f (rust-lang#89841):

* Match ergonomic tests adapted from the `rfc2005` test suite.

Added by fec8a50 (rust-lang#89841):

* [`ui/let-else/let-else-deref-coercion-annotated.rs`](https://github.com/rust-lang/rust/blob/1.61.0/src/test/ui/let-else/let-else-deref-coercion-annotated.rs) and [`ui/let-else/let-else-deref-coercion.rs`](https://github.com/rust-lang/rust/blob/1.61.0/src/test/ui/let-else/let-else-deref-coercion.rs) to check deref coercions.

#### Added since this stabilization report was originally written (2022-02-09)

Added by 76ea566 (rust-lang#94211):

* [`ui/let-else/let-else-destructuring.rs`](https://github.com/rust-lang/rust/blob/1.63.0/src/test/ui/let-else/let-else-destructuring.rs) to give a nice error message if an user tries to do an assignment with a (possibly refutable) pattern and an `else` block, like asked for in rust-lang#93995.

Added by e7730dc (rust-lang#94208):

* [`ui/let-else/let-else-allow-in-expr.rs`](https://github.com/rust-lang/rust/blob/1.61.0/src/test/ui/let-else/let-else-allow-in-expr.rs) to test whether `#[allow(unused_variables)]` works in the expr, as well as its non presence, as well as putting it on the entire `let else` *affects* the expr, too. This was adding a missing test as pointed out by the stabilization report.
* Expansion of `ui/let-else/let-else-allow-unused.rs` and `ui/let-else/let-else-check.rs` to ensure that non-presence of `#[allow(unused)]` does issue the unused lint. This was adding a missing test case as pointed out by the stabilization report.

Added by 5bd7106 (rust-lang#94208):

* [`ui/let-else/let-else-slicing-error.rs`](https://github.com/rust-lang/rust/blob/1.61.0/src/test/ui/let-else/let-else-slicing-error.rs), a regression test for rust-lang#92069, which got fixed without addition of a regression test. This resolves a missing test as pointed out by the stabilization report.

Added by 5374688 (rust-lang#98574):

* [`src/test/ui/async-await/async-await-let-else.rs`](https://github.com/rust-lang/rust/blob/master/src/test/ui/async-await/async-await-let-else.rs) to test the interaction of async/await with `let else`

Added by 6c529de (rust-lang#98574):

* [`src/test/ui/let-else/let-else-temporary-lifetime.rs`](https://github.com/rust-lang/rust/blob/master/src/test/ui/let-else/let-else-temporary-lifetime.rs) as a (partial) regression test for rust-lang#98672

Added by 9b56640 (rust-lang#99518):

* [`src/test/ui/let-else/let-else-temp-borrowck.rs`](https://github.com/rust-lang/rust/blob/master/src/test/ui/let-else/let-else-temporary-lifetime.rs) as a regression test for rust-lang#93951
* Extension of `src/test/ui/let-else/let-else-temporary-lifetime.rs` to include a partial regression test for rust-lang#98672 (especially regarding `else` drop order)

Added by baf9a7c (rust-lang#99518):

* Extension of `src/test/ui/let-else/let-else-temporary-lifetime.rs` to include a partial regression test for rust-lang#93951, similar to `let-else-temp-borrowck.rs`

Added by 60be2de (rust-lang#99518):

* Extension of `src/test/ui/let-else/let-else-temporary-lifetime.rs` to include a program that can now be compiled thanks to borrow checker implications of rust-lang#99518

Added by 47a7a91 (rust-lang#100132):

* [`src/test/ui/let-else/issue-100103.rs`](https://github.com/rust-lang/rust/blob/master/src/test/ui/let-else/issue-100103.rs), as a regression test for rust-lang#100103, to ensure that there is no ICE when doing `Err(...)?` inside else blocks.

Added by e3c5bd6 (rust-lang#100443):

* [`src/test/ui/let-else/let-else-then-diverge.rs`](https://github.com/rust-lang/rust/blob/master/src/test/ui/let-else/let-else-then-diverge.rs), to verify that there is no unreachable code error with the current desugaring.

Added by 9818526 (rust-lang#100443):

* [`src/test/ui/let-else/issue-94176.rs`](https://github.com/rust-lang/rust/blob/master/src/test/ui/let-else/issue-94176.rs), to make sure that a correct span is emitted for a missing trailing expression error. Regression test for rust-lang#94176.

Added by e182d12 (rust-lang#100434):

* [src/test/ui/unpretty/pretty-let-else.rs](https://github.com/rust-lang/rust/blob/master/src/test/ui/unpretty/pretty-let-else.rs), as a regression test to ensure pretty printing works for `let else` (this bug surfaced in many different ways)

Added by e262856 (rust-lang#99954):

* [`src/test/ui/let-else/let-else-temporary-lifetime.rs`](https://github.com/rust-lang/rust/blob/master/src/test/ui/let-else/let-else-temporary-lifetime.rs) extended to contain & borrows as well, as this was identified as an earlier issue with the desugaring: rust-lang#98672 (comment)

Added by 2d8460e (rust-lang#99291):

* [`src/test/ui/let-else/let-else-drop-order.rs`](https://github.com/rust-lang/rust/blob/master/src/test/ui/let-else/let-else-drop-order.rs) a matrix based test for various drop order behaviour of `let else`. Especially, it verifies equality of `let` and `let else` drop orders, [resolving](rust-lang#93628 (comment)) a [stabilization blocker](rust-lang#93628 (comment)).

Added by 1b87ce0 (rust-lang#101410):

* Edit to `src/test/ui/let-else/let-else-temporary-lifetime.rs` to add the `-Zvalidate-mir` flag, as a regression test for rust-lang#99228

Added by af591eb (rust-lang#101410):

* [`src/test/ui/let-else/issue-99975.rs`](https://github.com/rust-lang/rust/blob/master/src/test/ui/let-else/issue-99975.rs) as a regression test for the ICE rust-lang#99975.

Added by this PR:

* `ui/let-else/let-else.rs`, a simple run-pass check, similar to `ui/let-else/let-else-run-pass.rs`.

### Things not currently tested

* ~~The `#[allow(...)]` tests check whether allow works, but they don't check whether the non-presence of allow causes a lint to fire.~~ → *test added by e7730dc*
* ~~There is no `#[allow(...)]` test for the expression, as there are tests for the pattern and the else block.~~ → *test added by e7730dc*
* ~~`let-else-brace-before-else.rs` forbids the `let ... = {} else {}` pattern and there is a rustfix to obtain `let ... = ({}) else {}`. I'm not sure whether the `.fixed` files are checked by the tooling that they compile. But if there is no such check, it would be neat to make sure that `let ... = ({}) else {}` compiles.~~ → *test added by e7730dc*
* ~~rust-lang#92069 got closed as fixed, but no regression test was added. Not sure it's worth to add one.~~ → *test added by 5bd7106*
* ~~consistency between `let else` and `if let` regarding lifetimes and drop order: rust-lang#93628 (comment) → *test added by 2d8460e*

Edit: they are all tested now.

### Possible future work / Refutable destructuring assignments

[RFC 2909](https://rust-lang.github.io/rfcs/2909-destructuring-assignment.html) specifies destructuring assignment, allowing statements like `FooBar { a, b, c } = foo();`.
As it was stabilized, destructuring assignment only allows *irrefutable* patterns, which before the advent of `let else` were the only patterns that `let` supported.
So the combination of `let else` and destructuring assignments gives reason to think about extensions of the destructuring assignments feature that allow refutable patterns, discussed in rust-lang#93995.

A naive mapping of `let else` to destructuring assignments in the form of `Some(v) = foo() else { ... };` might not be the ideal way. `let else` needs a diverging `else` clause as it introduces new bindings, while assignments have a default behaviour to fall back to if the pattern does not match, in the form of not performing the assignment. Thus, there is no good case to require divergence, or even an `else` clause at all, beyond the need for having *some* introducer syntax so that it is clear to readers that the assignment is not a given (enums and structs look similar). There are better candidates for introducer syntax however than an empty `else {}` clause, like `maybe` which could be added as a keyword on an edition boundary:

```Rust
let mut v = 0;
maybe Some(v) = foo(&v);
maybe Some(v) = foo(&v) else { bar() };
```

Further design discussion is left to an RFC, or the linked issue.
@zeenix zeenix mentioned this pull request Aug 11, 2023
@Pzixel
Copy link

Pzixel commented Sep 28, 2023

I'm not sure if this issue was closed for good but I have a question if expansion of the syntax is considered to be implemented. I was actively using let else for cases when I don't care about errors, and it worked wonders, until I realized that even I Don't care about it I still want to log it. So for example:

let Ok((ws_stream, _)) = tokio_tungstenite::connect_async(&url).await else {
   tracing::warn!("[wss] Cannot connect to RPC endpoint. Waiting and reconnecting");
   tokio::time::sleep(reconnect_delay).await;
   continue;
 };

It's cool and beans but at some point you realize that you want to know why it is failing, so in order to do so you need to write:

let ws_stream = match tokio_tungstenite::connect_async(&url).await {
    Ok((ws_stream, _)) => ws_stream,
    Err(e) => {
        tracing::warn!("[wss] Cannot connect to RPC endpoint: {}. Waiting and reconnecting", e);
        tokio::time::sleep(reconnect_delay).await;
        continue;
    }
};

Which is quite verbose for such a simple task. Add some more logic and suddenly out of the function for 100 lines you have 10-20 eaten just by the following pattern, with a lot of foos like in let foo = match Ok(foo) => foo.

Approach with map_err looks really bad for any real error handling:

let Ok((ws_stream, _)) = tokio_tungstenite::connect_async(&url).await
    .map_err(|e| tracing::warn!("[wss] Cannot connect to RPC endpoint: {}. Waiting and reconnecting", e)) else {
   tokio::time::sleep(reconnect_delay).await;
   continue;
 };

It also splits error handling in multiple blocks which is harder to read (and write).

So for now the only way to use let else is when you don't care about non-matched value at all, which isn't the case in almost every scenario except for Option where you just know in advance it is None (just because of the type inhabitance).

Honestly I would like to have an optional binding so both programs I wrote below would be accepted:

let Ok((ws_stream, _)) = tokio_tungstenite::connect_async(&url).await else {
   tracing::warn!("[wss] Cannot connect to RPC endpoint. Waiting and reconnecting");
   tokio::time::sleep(reconnect_delay).await;
   continue;
 };

and

let Ok((ws_stream, _)) = tokio_tungstenite::connect_async(&url).await else e {
   tracing::warn!("[wss] Cannot connect to RPC endpoint: {:?}. Waiting and reconnecting", e);
   tokio::time::sleep(reconnect_delay).await;
   continue;
 };

Since it is currently an invalid program this feature can be added in backward-compatible manner. I know this was covered in https://github.com/Fishrock123/rfcs/blob/let-else/text/0000-let-else.md#let-else-match but I suggest to reiterate and see if it can be added as an optional feature.

@clarfonthey
Copy link
Contributor

One other thing to consider as a limitation of let-else is the fact that the new destructuring assignment doesn't work with it, meaning that if you wanted to do the same as the above without the let (assigning to an existing ws_stream variable, and not binding a new one), it would not be possible.

@Fishrock123
Copy link
Contributor Author

@Pzixel The answer to this is suggested to be to use match as you mentioned.

The problem with e.g. let PATTERN: TYPE = EXPRESSION else (PATTERN) DIVERGING_BLOCK; is that, is short, it implicitly can only work for patterns with two possible variants.

There could be an argument that this is still useful, however it mostly seems that this happens with Option and Result and in my experience there is usually a better / more idiomatic option available (via chaining operations, ?, or match). I do not think it would be useful to extend support for this to multiple else clauses - the code is not going to be more succinct than just using match.

I do agree with this sentiment regarding map_err:

It also splits error handling in multiple blocks which is harder to read (and write).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. finished-final-comment-period The final comment period is finished for this RFC. T-lang Relevant to the language team, which will review and decide on the RFC. to-announce
Projects
None yet
Development

Successfully merging this pull request may close these issues.