From 2b92ef583be8521ec9506cb11d99a02963ffb398 Mon Sep 17 00:00:00 2001 From: Jeremiah Senkpiel Date: Mon, 31 May 2021 18:07:41 -0700 Subject: [PATCH 01/25] let-else first draft --- text/0000-let-else.md | 329 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 329 insertions(+) create mode 100644 text/0000-let-else.md diff --git a/text/0000-let-else.md b/text/0000-let-else.md new file mode 100644 index 00000000000..e019e52f80a --- /dev/null +++ b/text/0000-let-else.md @@ -0,0 +1,329 @@ +- Feature Name: `let-else` +- Start Date: 2021-05-31 +- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000) +- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) + +# Summary +[summary]: #summary + +Introduce a new `let PATTERN = 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 (e.g. return or break). +let-else statements are refutable `let` statements. + +This RFC is a modernization of a [2015 RFC (pull request 1303)][old-rfc] for an almost identical feature. + +# Motivation +[motivation]: #motivation + +`let else` simplifies some very common error-handling patterns. +It is the natural counterpart to `if let`, just as `else` is to regular `if`. + +[if-let expressions][if-let] offer a succinct syntax for pattern matching single patterns. +This is particularly useful for unwrapping types like `Option`, particularly those with a clear "success" varient +for the given context but no specific "failure" varient. +However, an if-let expression can only create bindings within its body, which can force +rightward drift, introduce excessive nesting, and separate conditionals from error paths. + +let-else statements move the "failure" case into the body block, while allowing +the "success" case to continue in the surrounding context without additional nesting. + +let-else statements are also more succinct and natural than emulating the equivalent pattern with `match` or if-let, +which require intermediary bindings (usually of the same name). + +## Examples + +The following three code examples are possible options with current Rust code. + +```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"); +} +``` + +```rust +let a = match x { + Some(a) => a, + _ => return Err("bad x"), +} +let b = match y { + Some(b) => b, + _ => return Err("bad y"), +} +let c = match z { + Some(c) => c, + _ => return Err("bad z"), +} +// ... +do_something_with(a, b, c); +// ... +``` + +```rust +let a = if let Some(a) { a } else { + return Err("bad x"), +}; +let b = if let Some(b) { b } else { + return Err("bad y"), +}; +let c = if let Some(c) { c } else { + return Err("bad z"), +}; +// ... +do_something_with(a, b, c); +// ... +``` + +All three of the above examples would be able to be written as: + +```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); +// ... +``` + +which succinctly avoids bindings of the same name, rightward shift, etc. + +## Versus `match` + +It is 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 +} +``` + +# Guide-level explanation +[guide-level-explanation]: #guide-level-explanation + +A common pattern in non-trivial code where static guarentees can not be fully met (e.g. I/O, network or otherwise) is to check error cases when possible before proceding, +and "return early", by constructing an error `Result` or an empty `Option`, and returning it before the "happy path" code. + +This pattern serves no practical purpose to a computer, but it is helpful for humans interacting with the code. +Returning early helps improve code clarity in two ways: +- Ensuring the returned result in near the conditional, visually, as the following logic may be lengthy. +- Reduces rightward shift, as the error return is now in the block, rather than the following logic. + +This RFC proposes _(Rust provides)_ an extension to `let` assignment statements to help with this pattern, an `else { }` which can follow a pattern match +as a `let` assigning statement: + +```rust +let Some(a) = an_option else { + // Called if `an_option` is not `Option::Some(T)`. + // This block must diverge (stop executing the existing context to the parent block or function). + return; +}; + +// `a` is now in scope and is the type which the `Option` contained. +``` + +This is a counterpart to `if let` expressions, and the pattern matching works identically, except that the value from the pattern match +is assigned to the surrounding scope rather than the block's scope. + +# Reference-level explanation +[reference-level-explanation]: #reference-level-explanation + +let-else is syntactical sugar for either `if let { assignment } else {}` or `match`, where the non-matched case diverges. + +Any expression may be put into the expression position except an `if {} else {}` as explain below in [drawbacks][]. +While `if {} else {}` is technically feasible this RFC proposes it be disallowed for programmer clarity to avoid an `... else {} else {}` situation. + +Any pattern that could be put into if-let's pattern position can be put into let-else's pattern position. + +The `else` block must diverge. This could be a keyword which diverges (returns `!`), or a panic. +Allowed keywords: +- `return` +- `break` +- `continue` + +If the pattern does not match, the expression is not consumed, and so any existing variables from the surrounding scope are +accessible as they would normally be. + +# Drawbacks +[drawbacks]: #drawbacks + +## The diverging block + +"Must diverge" is an unusual requirement, which doesn't exist elsewhere in the language as of the time of writing, +and might be difficult to explain or lead to confusing errors for programmers new to this feature. + +## `let PATTERN = if {} else {} else {};` + +One unfortunate combination of this feature with regular if-else expressions is the possibility of `let PATTERN = if { a } else { b } else { c };`. +This is likely to be unclear if anyone writes it, but does not pose a syntactical issue, as `let PATTERN = if y { a } else { b };` should always be +interperited as `let Enum(x) = (if y { a } else { b });` (still a compile error as there no diverging block: `error[E0005]: refutable pattern in local binding: ...`) +because the compiler won't interpret it as `let PATTERN = (if y { a }) else { b };` since `()` is not an enum. + +This can be overcome by making a raw if-else in the expression position a compile error and instead requring that parentheses are inserted to disambiguate: +`let PATTERN = (if { a } else { b }) else { c };`. + +# Rationale and alternatives +[rationale-and-alternatives]: #rationale-and-alternatives + +let-else attempts to be as consistent as possible to similar existing syntax. + +Fundimentally it is treated as a `let` statement, necessitating an assignment and the trailing semicolon. + +Pattern matching works identically to if-let, no new "negation" pattern matching rules are introduced. + +The `else` must be followed by a block, as in `if {} else {}`. + +The else block must be diverging as the outer context cannot be guarenteed to continue soundly without assignment, and no alternate assignment syntax is provided. + +While this feature can effectively be covered by functions such `or_or`/`ok_or_else` on the `Option` and `Result` types combined with the Try operator (`?`), +such functions do not exist automatically on custom enum types and require non-obvious and non-trivial implementation, and may not be map-able +to `Option`/`Result`-style functions at all (especially for enums where the "success" varient is contextual and there are many varients). + +## Alternatives + +### `let PATTERN = EXPR else return EXPR;` + +A potential alternative to requiring parentheses in `let PATTERN = (if { a } else { b }) else { c };` is to change the syntax of the `else` to no longer be a block +but instead an expression which starts with a diverging keyword, such as `return` or `break`. + +Example: +``` +let Some(foo) = some_option else return None; +``` + +This RFC avoids this because it is overall less consistent with `else` from if-else, which require blocks. + +This was originally suggested in the old RFC, comment at https://github.com/rust-lang/rfcs/pull/1303#issuecomment-188526691 + +### `else`-block fall-back assignment + +A fall-back assignment alternate to the diverging block has been proposed multiple times in relation to this feature in the [original rfc][] and also in out-of-RFC discussions. + +This RFC avoids this proposal, because there is no clear syntax to use for it which would be consistent with other existing features. +Also use-cases for having a single fall-back are much more rare and ususual, where as use cases for the diverging block are very common. +This RFC proposes that most fallback cases are sufficiently or better covered by using `match`. + +An example, using a proposal to have the binding be visible and assignable from the `else`-block. +Note that this is incompatible with this RFC and could probably not be added as an extension from this RFC. + +```rust +enum AnEnum { + Varient1(u32), + Varient2(String), +} + +let AnEnum::Varient1(a) = x else { + a = 42; +}; +``` + +Another potential alternative for fall-back which could be added with an additional keyword as a future extension: + +```rust +enum AnEnum { + Varient1(u32), + Varient2(String), +} + +let AnEnum::Varient1(a) = x else assign a { + a = 42; +}; +``` + +### `if !let PAT = EXPR { BODY }` + +The [old RFC][old-rfc] originally proposed this general feature via some kind of pattern negation as `if !let PAT = EXPR { BODY }`. + +This RFC avoids adding any kind of new or special pattern matching rules. The pattern matching works as it does for if-let. +The general consensus in the old RFC was also that the negation syntax is much less clear than `if PATTERN = EXPR else { /* diverge */ };`, +and partway through that RFC's lifecycle it was updated to be similar to this RFC's proposed let-else syntax. + +### Complete Alternative + +- Don't make any changes; use existing syntax like `if let` and `match` as shown in the motivating example, or write macros to simplify the code. + +# Prior art +[prior-art]: #prior-art + +This RFC is a modernization of a [2015 RFC (pull request 1303)][old-rfc]. + +A lot of this RFC's proposals come from that RFC and its ensuing discussions. + +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. + +The `match` alternative in particular is fairly prevalent in rust code on projects which have many possible error conditions. + +The Try operator allows for an `ok_or` alternative to be used where the types are only `Option` and `Result`, +which is considered to be idomatic rust. + +// TODO link to examples, provide internal stistics, gather statistics from the rust compiler itself, etc. + +# Unresolved questions +[unresolved-questions]: #unresolved-questions + +None known at time of writing due to extensive pre-discussion in Zulip: +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 + +# Future possibilities +[future-possibilities]: #future-possibilities + +## Fall-back assignment + +This RFC does not suggest that we do any of these, but notes that they would be future possibilities. + +If fall-back assignment as discussed above in [rationale-and-alternatives][] is desirable, it could be added with an additional keyword as a future extension: + +```rust +enum AnEnum { + Varient1(u32), + Varient2(String), +} + +let AnEnum::Varient1(a) = x else assign a { + a = 42; +}; +``` + +Another potential form of the fall-back extension: + +```rust +let Ok(a) = x else match { + Err(e) => return Err(e.into()), +} +``` + +[old-rfc]: https://github.com/rust-lang/rfcs/pull/1303 +[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 From 3438586767d104cc48087329c2402a1416507bc7 Mon Sep 17 00:00:00 2001 From: Jeremiah Senkpiel Date: Tue, 1 Jun 2021 17:29:11 -0700 Subject: [PATCH 02/25] let-else draft updates from zulip Specifically bstrie's suggestion of `ExpressionWithoutBlock`. Also some spelling fixes and example updates. --- text/0000-let-else.md | 122 +++++++++++++++++++++++++++--------------- 1 file changed, 79 insertions(+), 43 deletions(-) diff --git a/text/0000-let-else.md b/text/0000-let-else.md index e019e52f80a..fa0172a70a1 100644 --- a/text/0000-let-else.md +++ b/text/0000-let-else.md @@ -6,7 +6,7 @@ # Summary [summary]: #summary -Introduce a new `let PATTERN = EXPRESSION else { /* DIVERGING BLOCK */ };` construct (informally called a +Introduce a new `let PATTERN = EXPRESSION_WITHOUT_BLOCK 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 @@ -22,8 +22,8 @@ This RFC is a modernization of a [2015 RFC (pull request 1303)][old-rfc] for an It is the natural counterpart to `if let`, just as `else` is to regular `if`. [if-let expressions][if-let] offer a succinct syntax for pattern matching single patterns. -This is particularly useful for unwrapping types like `Option`, particularly those with a clear "success" varient -for the given context but no specific "failure" varient. +This is particularly useful for unwrapping types like `Option`, particularly those with a clear "success" variant +for the given context but no specific "failure" variant. However, an if-let expression can only create bindings within its body, which can force rightward drift, introduce excessive nesting, and separate conditionals from error paths. @@ -35,7 +35,7 @@ which require intermediary bindings (usually of the same name). ## Examples -The following three code examples are possible options with current Rust code. +The following two code examples are possible options with current Rust code. ```rust if let Some(a) = x { @@ -73,22 +73,7 @@ do_something_with(a, b, c); // ... ``` -```rust -let a = if let Some(a) { a } else { - return Err("bad x"), -}; -let b = if let Some(b) { b } else { - return Err("bad y"), -}; -let c = if let Some(c) { c } else { - return Err("bad z"), -}; -// ... -do_something_with(a, b, c); -// ... -``` - -All three of the above examples would be able to be written as: +Both of the above examples would be able to be written as: ```rust let Some(a) = x else { @@ -107,6 +92,49 @@ do_something_with(a, b, c); which succinctly avoids bindings of the same name, rightward shift, etc. +let-else is even more useful when dealing with enums which are not `Option`/`Result`, consider how the +following code would look without let-else (transposed from a real-world project written in part by the author): + +```rust +impl ActionView { + pub(crate) fn new(history: &History) -> Result { + let mut iter = history.iter(); + let event = iter + .next() + .ok_or_else(|| eyre::eyre!("Entity has no history"))?; + + let Action::Register { + actor: String, + x: Vec + y: u32, + z: String, + } = event.action().clone() else { + // RFC Author's note: + // Without if-else this was separated from the conditional + // by a substantial block of code which now follows below. + Err(eyre::eyre!("must begin with a Register action")) + }; + + let created = *event.created(); + let mut view = ActionView { + registered_by: (actor, created), + a: (actor.clone(), x, created), + b: (actor.clone(), y, created), + c: (z, created), + d: Vec::new(), + + e: None, + f: None, + g: None, + }; + for event in iter { + view.update(&event)?; + } + Ok(view) + } +} +``` + ## Versus `match` It is possible to use `match` statements to emulate this today, but at a @@ -131,7 +159,7 @@ let Some(ref subpage_layer_info) = layer_properties.subpage_layer_info else { # Guide-level explanation [guide-level-explanation]: #guide-level-explanation -A common pattern in non-trivial code where static guarentees can not be fully met (e.g. I/O, network or otherwise) is to check error cases when possible before proceding, +A common pattern in non-trivial code where static guarantees can not be fully met (e.g. I/O, network or otherwise) is to check error cases when possible before proceeding, and "return early", by constructing an error `Result` or an empty `Option`, and returning it before the "happy path" code. This pattern serves no practical purpose to a computer, but it is helpful for humans interacting with the code. @@ -155,17 +183,19 @@ let Some(a) = an_option else { This is a counterpart to `if let` expressions, and the pattern matching works identically, except that the value from the pattern match is assigned to the surrounding scope rather than the block's scope. -# Reference-level explanation +# Reference-level explanations [reference-level-explanation]: #reference-level-explanation let-else is syntactical sugar for either `if let { assignment } else {}` or `match`, where the non-matched case diverges. Any expression may be put into the expression position except an `if {} else {}` as explain below in [drawbacks][]. While `if {} else {}` is technically feasible this RFC proposes it be disallowed for programmer clarity to avoid an `... else {} else {}` situation. +Rust already provides us with such a restriction, [`ExpressionWithoutBlock`][expressions]. Any pattern that could be put into if-let's pattern position can be put into let-else's pattern position. The `else` block must diverge. This could be a keyword which diverges (returns `!`), or a panic. +This likely necessitates a new subtype of `BlockExpression`, something like `BlockExpressionDiverging`. Allowed keywords: - `return` - `break` @@ -182,34 +212,39 @@ accessible as they would normally be. "Must diverge" is an unusual requirement, which doesn't exist elsewhere in the language as of the time of writing, and might be difficult to explain or lead to confusing errors for programmers new to this feature. +This also neccesitates a new block expression subtype, something like `BlockExpressionDiverging`. + ## `let PATTERN = if {} else {} else {};` One unfortunate combination of this feature with regular if-else expressions is the possibility of `let PATTERN = if { a } else { b } else { c };`. This is likely to be unclear if anyone writes it, but does not pose a syntactical issue, as `let PATTERN = if y { a } else { b };` should always be -interperited as `let Enum(x) = (if y { a } else { b });` (still a compile error as there no diverging block: `error[E0005]: refutable pattern in local binding: ...`) +interpreted as `let Enum(x) = (if y { a } else { b });` (still a compile error as there no diverging block: `error[E0005]: refutable pattern in local binding: ...`) because the compiler won't interpret it as `let PATTERN = (if y { a }) else { b };` since `()` is not an enum. -This can be overcome by making a raw if-else in the expression position a compile error and instead requring that parentheses are inserted to disambiguate: +This can be overcome by making a raw if-else in the expression position a compile error and instead requiring that parentheses are inserted to disambiguate: `let PATTERN = (if { a } else { b }) else { c };`. +Rust already provides us with such a restriction, and so the expression can be restricted to be a [`ExpressionWithoutBlock`][expressions]. + # Rationale and alternatives [rationale-and-alternatives]: #rationale-and-alternatives let-else attempts to be as consistent as possible to similar existing syntax. -Fundimentally it is treated as a `let` statement, necessitating an assignment and the trailing semicolon. +Fundamentally it is treated as a `let` statement, necessitating an assignment and the trailing semicolon. Pattern matching works identically to if-let, no new "negation" pattern matching rules are introduced. -The `else` must be followed by a block, as in `if {} else {}`. +The expression can be any [`ExpressionWithoutBlock`][expressions], in order to prevent `else {} else {}` confusion, as noted in [drawbacks][#drawbacks]. + +The `else` must be followed by a block, as in `if {} else {}`. This else block must be diverging as the outer +context cannot be guaranteed to continue soundly without assignment, and no alternate assignment syntax is provided. -The else block must be diverging as the outer context cannot be guarenteed to continue soundly without assignment, and no alternate assignment syntax is provided. +## Alternatives While this feature can effectively be covered by functions such `or_or`/`ok_or_else` on the `Option` and `Result` types combined with the Try operator (`?`), such functions do not exist automatically on custom enum types and require non-obvious and non-trivial implementation, and may not be map-able -to `Option`/`Result`-style functions at all (especially for enums where the "success" varient is contextual and there are many varients). - -## Alternatives +to `Option`/`Result`-style functions at all (especially for enums where the "success" variant is contextual and there are many variants). ### `let PATTERN = EXPR else return EXPR;` @@ -230,7 +265,7 @@ This was originally suggested in the old RFC, comment at https://github.com/rust A fall-back assignment alternate to the diverging block has been proposed multiple times in relation to this feature in the [original rfc][] and also in out-of-RFC discussions. This RFC avoids this proposal, because there is no clear syntax to use for it which would be consistent with other existing features. -Also use-cases for having a single fall-back are much more rare and ususual, where as use cases for the diverging block are very common. +Also use-cases for having a single fall-back are much more rare and unusual, where as use cases for the diverging block are very common. This RFC proposes that most fallback cases are sufficiently or better covered by using `match`. An example, using a proposal to have the binding be visible and assignable from the `else`-block. @@ -238,11 +273,11 @@ Note that this is incompatible with this RFC and could probably not be added as ```rust enum AnEnum { - Varient1(u32), - Varient2(String), + Variant1(u32), + Variant2(String), } -let AnEnum::Varient1(a) = x else { +let AnEnum::Variant1(a) = x else { a = 42; }; ``` @@ -251,11 +286,11 @@ Another potential alternative for fall-back which could be added with an additio ```rust enum AnEnum { - Varient1(u32), - Varient2(String), + Variant1(u32), + Variant2(String), } -let AnEnum::Varient1(a) = x else assign a { +let AnEnum::Variant1(a) = x else assign a { a = 42; }; ``` @@ -265,7 +300,7 @@ let AnEnum::Varient1(a) = x else assign a { The [old RFC][old-rfc] originally proposed this general feature via some kind of pattern negation as `if !let PAT = EXPR { BODY }`. This RFC avoids adding any kind of new or special pattern matching rules. The pattern matching works as it does for if-let. -The general consensus in the old RFC was also that the negation syntax is much less clear than `if PATTERN = EXPR else { /* diverge */ };`, +The general consensus in the old RFC was also that the negation syntax is much less clear than `if PATTERN = EXPR_WITHOUT_BLOCK else { /* diverge */ };`, and partway through that RFC's lifecycle it was updated to be similar to this RFC's proposed let-else syntax. ### Complete Alternative @@ -286,9 +321,9 @@ proposal except for the choice of keywords. The `match` alternative in particular is fairly prevalent in rust code on projects which have many possible error conditions. The Try operator allows for an `ok_or` alternative to be used where the types are only `Option` and `Result`, -which is considered to be idomatic rust. +which is considered to be idiomatic rust. -// TODO link to examples, provide internal stistics, gather statistics from the rust compiler itself, etc. +// TODO link to examples, provide internal statistics, gather statistics from the rust compiler itself, etc. # Unresolved questions [unresolved-questions]: #unresolved-questions @@ -307,11 +342,11 @@ If fall-back assignment as discussed above in [rationale-and-alternatives][] is ```rust enum AnEnum { - Varient1(u32), - Varient2(String), + Variant1(u32), + Variant2(String), } -let AnEnum::Varient1(a) = x else assign a { +let AnEnum::Variant1(a) = x else assign a { a = 42; }; ``` @@ -324,6 +359,7 @@ let Ok(a) = x else match { } ``` +[expressions]: https://doc.rust-lang.org/reference/expressions.html#expressions [old-rfc]: https://github.com/rust-lang/rfcs/pull/1303 [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 From c131adb426f23053c6878d531411465ca6676a53 Mon Sep 17 00:00:00 2001 From: Jeremiah Senkpiel Date: Wed, 2 Jun 2021 10:35:04 -0700 Subject: [PATCH 03/25] let-else draft updates for if-let-chains Thanks to Frank Steffahn for pointing this out in Zulip. --- text/0000-let-else.md | 98 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 88 insertions(+), 10 deletions(-) diff --git a/text/0000-let-else.md b/text/0000-let-else.md index fa0172a70a1..2ec471de27e 100644 --- a/text/0000-let-else.md +++ b/text/0000-let-else.md @@ -11,7 +11,7 @@ Introduce a new `let PATTERN = EXPRESSION_WITHOUT_BLOCK else DIVERGING_BLOCK;` c 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 (e.g. return or break). -let-else statements are refutable `let` statements. +Technically speaking, let-else statements are refutable `let` statements. This RFC is a modernization of a [2015 RFC (pull request 1303)][old-rfc] for an almost identical feature. @@ -112,7 +112,7 @@ impl ActionView { // RFC Author's note: // Without if-else this was separated from the conditional // by a substantial block of code which now follows below. - Err(eyre::eyre!("must begin with a Register action")) + return Err(eyre::eyre!("must begin with a Register action")); }; let created = *event.created(); @@ -212,7 +212,7 @@ accessible as they would normally be. "Must diverge" is an unusual requirement, which doesn't exist elsewhere in the language as of the time of writing, and might be difficult to explain or lead to confusing errors for programmers new to this feature. -This also neccesitates a new block expression subtype, something like `BlockExpressionDiverging`. +This also necessitates a new block expression subtype, something like `BlockExpressionDiverging`. ## `let PATTERN = if {} else {} else {};` @@ -235,17 +235,42 @@ Fundamentally it is treated as a `let` statement, necessitating an assignment an Pattern matching works identically to if-let, no new "negation" pattern matching rules are introduced. +Operator precedence with `&&` in made to be like if-let, requiring that a case which is an error prior to this RFC be changed to be a slightly different error. +This is for a possible extension for let-else similar to the (yet unimplemented) if-else-chains feature, as mentioned in [future-possibilities][] with more detail. +Specifically, while the following example is an error today, by the default `&&` operator rules it would cause problems with if-let-chains like `&&` chaining: + +```rust +let a = false; +let b = false; + +// The RFC proposes boolean matches like this be either: +// - Made into a compile error, or +// - Made to be parsed like if-let-chains: `(true = a) && b` +let true = a && b else { + return; +}; +``` + The expression can be any [`ExpressionWithoutBlock`][expressions], in order to prevent `else {} else {}` confusion, as noted in [drawbacks][#drawbacks]. The `else` must be followed by a block, as in `if {} else {}`. This else block must be diverging as the outer context cannot be guaranteed to continue soundly without assignment, and no alternate assignment syntax is provided. - ## Alternatives While this feature can effectively be covered by functions such `or_or`/`ok_or_else` on the `Option` and `Result` types combined with the Try operator (`?`), such functions do not exist automatically on custom enum types and require non-obvious and non-trivial implementation, and may not be map-able to `Option`/`Result`-style functions at all (especially for enums where the "success" variant is contextual and there are many variants). +### `unless let ... {}` / `try let ... {}` + +An often proposed alternative is to add an extra keyword to the beginning of the let-else statement, to denote that it is different than a regular `let` statement. + +One possible benefit of adding a keyword is that it could make a possible future extension for similarity to the (yet unimplemented) [if-let-chains][] feature more straightforward. +However, as mentioned in the [future-possibilities][] section, this is likely not necessary. + +This syntax has prior art in the Swift programming language, which includes a [guard-let-else][swift] statement +which is roughly equivalent to this proposal except for the choice of keywords. + ### `let PATTERN = EXPR else return EXPR;` A potential alternative to requiring parentheses in `let PATTERN = (if { a } else { b }) else { c };` is to change the syntax of the `else` to no longer be a block @@ -305,7 +330,7 @@ and partway through that RFC's lifecycle it was updated to be similar to this RF ### Complete Alternative -- Don't make any changes; use existing syntax like `if let` and `match` as shown in the motivating example, or write macros to simplify the code. +Don't make any changes; use existing syntax like `match` (or `if let`) as shown in the motivating example, or write macros to simplify the code. # Prior art [prior-art]: #prior-art @@ -315,16 +340,14 @@ This RFC is a modernization of a [2015 RFC (pull request 1303)][old-rfc]. A lot of this RFC's proposals come from that RFC and its ensuing discussions. The Swift programming language, which inspired Rust's if-let expression, also -includes a [guard-let-else][swift] statement which is equivalent to this +includes a [guard-let-else][swift] statement which is roughly equivalent to this proposal except for the choice of keywords. The `match` alternative in particular is fairly prevalent in rust code on projects which have many possible error conditions. -The Try operator allows for an `ok_or` alternative to be used where the types are only `Option` and `Result`, +The Try operator allows for an `ok_or_else` alternative to be used where the types are only `Option` and `Result`, which is considered to be idiomatic rust. -// TODO link to examples, provide internal statistics, gather statistics from the rust compiler itself, etc. - # Unresolved questions [unresolved-questions]: #unresolved-questions @@ -334,6 +357,60 @@ https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/topic/.60let.20patt # Future possibilities [future-possibilities]: #future-possibilities +## if-let-chains + +An RFC exists for a (unimplemented at time of writing) feature called [if-let-chains][]: + +```rust +if let Some(foo) = expr() && foo.is_baz() && let Ok(yay) = qux(foo) { ... } +``` + +While this RFC does not introduce or propose the same thing for let-else it attempts to allow it to be a future possibility for +potential future consistency with if-let-chains. + +The primary obstacle is existing operator order precedence. +Given the above example, it would likely be parsed as follows with ordinary operator precedence rules for `&&`: +```rust +let Some(foo) = (expr() && foo.is_baz() && let Ok(yay) = qux(foo) else { ... }) +``` + +However, given that all existing occurrences of this behavior before this RFC are type errors anyways, +a specific boolean-only case can be avoided and thus parsing can be changed to lave the door open to this possible extension. +This boolean case is always equivalent to a less flexible `if` statement and as such is not useful. + +```rust +let maybe = Some(2); +let has_thing = true; + +// Always an error regardless, because && only operates on booleans. +let Some(x) = maybe && has_thing else { + return; +}; +``` + +```rust +let a = false; +let b = false; + +// The RFC proposes boolean matches like this be either: +// - Made into a compile error, or +// - Made to be parsed like if-let-chains: `(true = a) && b` +let true = a && b else { + return; +}; +``` + +Note also that this does not work today either, because booleans are refutable patterns: +``` +error[E0005]: refutable pattern in local binding: `false` not covered + --> src/main.rs:5:9 + | +5 | let true = a && b; + | ^^^^ pattern `false` not covered + | + = note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant +``` + ## Fall-back assignment This RFC does not suggest that we do any of these, but notes that they would be future possibilities. @@ -361,5 +438,6 @@ let Ok(a) = x else match { [expressions]: https://doc.rust-lang.org/reference/expressions.html#expressions [old-rfc]: https://github.com/rust-lang/rfcs/pull/1303 -[if-let]: https://github.com/rust-lang/rfcs/blob/master/text/0160-if-let.md +[if-let]: https://rust-lang.github.io/rfcs/0160-if-let.html +[if-let-chains]: https://rust-lang.github.io/rfcs/2497-if-let-chains.html [swift]: https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/ControlFlow.html#//apple_ref/doc/uid/TP40014097-CH9-ID525 From c9e75dc16b28154110d5355997e53ac7e918dbbf Mon Sep 17 00:00:00 2001 From: Jeremiah Senkpiel Date: Wed, 2 Jun 2021 11:06:54 -0700 Subject: [PATCH 04/25] let-else draft updates for practical refactor example --- text/0000-let-else.md | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/text/0000-let-else.md b/text/0000-let-else.md index 2ec471de27e..92f6732fd44 100644 --- a/text/0000-let-else.md +++ b/text/0000-let-else.md @@ -137,7 +137,7 @@ impl ActionView { ## Versus `match` -It is possible to use `match` statements to emulate this today, but at a +It is possible to use `match` expressions to emulate this today, but at a significant cost in length and readability. For example, this real-world code from Servo: @@ -156,6 +156,40 @@ let Some(ref subpage_layer_info) = layer_properties.subpage_layer_info else { } ``` +## A practical refactor + +A refactor on an http server codebase in part written by the author to move some if-let conditionals to early-return `match` expressions +yielded 4 changes of large if-let blocks over `Option`s to use `ok_or_else` + `?`, and 5 changed to an early-return `match`. +The commit of the refactor was +531 −529 lines of code over a codebase of 4111 lines of rust code. +The largest block was 90 lines of code which was able to be shifted to the left, and have its error case moved up to the conditional, +showing the value of early-returns for this kind of program. + +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: + +```rust +let features = match geojson { + GeoJson::FeatureCollection(features) => features, + _ => { + return Err(format_err_status!( + 422, + "GeoJSON was not a Feature Collection", + )); + } +}; +``` + +However, with if-let this could be very succinct: + +```rust +let GeoJson::FeatureCollection(features) = geojson else { + return Err(format_err_status!( + 422, + "GeoJSON was not a Feature Collection", + )); +}; +``` + # Guide-level explanation [guide-level-explanation]: #guide-level-explanation From d9e8bd45730b361e7fe1862abfebbf9214ab0ebb Mon Sep 17 00:00:00 2001 From: Jeremiah Senkpiel Date: Wed, 2 Jun 2021 13:39:12 -0700 Subject: [PATCH 05/25] let-else draft, add desugar example By request of Josh Triplett --- text/0000-let-else.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/text/0000-let-else.md b/text/0000-let-else.md index 92f6732fd44..8021b5bf8cb 100644 --- a/text/0000-let-else.md +++ b/text/0000-let-else.md @@ -220,7 +220,21 @@ is assigned to the surrounding scope rather than the block's scope. # Reference-level explanations [reference-level-explanation]: #reference-level-explanation -let-else is syntactical sugar for either `if let { assignment } else {}` or `match`, where the non-matched case diverges. +let-else is syntactical sugar for `match` where the non-matched case diverges. +```rust +let pattern = expr else { + /* diverging expr */ +}; +``` +desugars to +```rust +let (each, binding) = match expr { + pattern => (each, binding), + _ => { + /* diverging expr */ + } +}; +``` Any expression may be put into the expression position except an `if {} else {}` as explain below in [drawbacks][]. While `if {} else {}` is technically feasible this RFC proposes it be disallowed for programmer clarity to avoid an `... else {} else {}` situation. From b5c3456d3e5b8e31b5d53172f5a1378aaa59b9da Mon Sep 17 00:00:00 2001 From: Jeremiah Senkpiel Date: Wed, 2 Jun 2021 15:05:24 -0700 Subject: [PATCH 06/25] let-else draft, fix let-if-chains example --- text/0000-let-else.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/text/0000-let-else.md b/text/0000-let-else.md index 8021b5bf8cb..5445e94ac7a 100644 --- a/text/0000-let-else.md +++ b/text/0000-let-else.md @@ -293,7 +293,7 @@ let b = false; // The RFC proposes boolean matches like this be either: // - Made into a compile error, or -// - Made to be parsed like if-let-chains: `(true = a) && b` +// - Made to be parsed internally like if-let-chains: `(let true = a) && b else { ... };` let true = a && b else { return; }; @@ -442,7 +442,7 @@ let b = false; // The RFC proposes boolean matches like this be either: // - Made into a compile error, or -// - Made to be parsed like if-let-chains: `(true = a) && b` +// - Made to be parsed internally like if-let-chains: `(let true = a) && b else { ... };` let true = a && b else { return; }; From 059587f13031a17f0333d774aba439cd3ddd3bfb Mon Sep 17 00:00:00 2001 From: Jeremiah Senkpiel Date: Thu, 3 Jun 2021 10:29:11 -0700 Subject: [PATCH 07/25] let-else draft, remove old example Also mentions the diverging return type of `!` in the summary. --- text/0000-let-else.md | 26 ++++---------------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/text/0000-let-else.md b/text/0000-let-else.md index 5445e94ac7a..801d6db7198 100644 --- a/text/0000-let-else.md +++ b/text/0000-let-else.md @@ -10,7 +10,7 @@ Introduce a new `let PATTERN = EXPRESSION_WITHOUT_BLOCK else DIVERGING_BLOCK;` c **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 (e.g. return or break). +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. This RFC is a modernization of a [2015 RFC (pull request 1303)][old-rfc] for an almost identical feature. @@ -135,28 +135,10 @@ impl ActionView { } ``` -## Versus `match` +## A practical refactor with `match` It is possible to use `match` expressions 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 -} -``` - -## A practical refactor +significant cost in length and readability. A refactor on an http server codebase in part written by the author to move some if-let conditionals to early-return `match` expressions yielded 4 changes of large if-let blocks over `Option`s to use `ok_or_else` + `?`, and 5 changed to an early-return `match`. @@ -179,7 +161,7 @@ let features = match geojson { }; ``` -However, with if-let this could be very succinct: +However, with if-let this could be more succinct & clear: ```rust let GeoJson::FeatureCollection(features) = geojson else { From 93c1f7ccc39abd8881324f9e75498a25cc0e0434 Mon Sep 17 00:00:00 2001 From: Jeremiah Senkpiel Date: Thu, 3 Jun 2021 12:27:04 -0700 Subject: [PATCH 08/25] let-else: rfc PR link --- text/0000-let-else.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-let-else.md b/text/0000-let-else.md index 801d6db7198..9abe1222355 100644 --- a/text/0000-let-else.md +++ b/text/0000-let-else.md @@ -1,6 +1,6 @@ - Feature Name: `let-else` - Start Date: 2021-05-31 -- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000) +- RFC PR: [rust-lang/rfcs#3137](https://github.com/rust-lang/rfcs/pull/3137) - Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) # Summary From 836f2c4828709680f08bedd5e5175b8ceecb73fd Mon Sep 17 00:00:00 2001 From: Jeremiah Senkpiel Date: Thu, 3 Jun 2021 14:10:44 -0700 Subject: [PATCH 09/25] let-else: inital updates from Josh Co-Authored-By: Josh Triplett --- text/0000-let-else.md | 46 ++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/text/0000-let-else.md b/text/0000-let-else.md index 9abe1222355..0e7cc47e0e3 100644 --- a/text/0000-let-else.md +++ b/text/0000-let-else.md @@ -59,15 +59,15 @@ if let Some(a) = x { let a = match x { Some(a) => a, _ => return Err("bad x"), -} +}; let b = match y { Some(b) => b, _ => return Err("bad y"), -} +}; let c = match z { Some(c) => c, _ => return Err("bad z"), -} +}; // ... do_something_with(a, b, c); // ... @@ -78,13 +78,13 @@ Both of the above examples would be able to be written as: ```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); // ... @@ -275,7 +275,7 @@ let b = false; // The RFC proposes boolean matches like this be either: // - Made into a compile error, or -// - Made to be parsed internally like if-let-chains: `(let true = a) && b else { ... };` +// - Made to be parsed internally like if-let-chains: `((let true = a) && b) else { ... };` let true = a && b else { return; }; @@ -285,11 +285,13 @@ The expression can be any [`ExpressionWithoutBlock`][expressions], in order to p The `else` must be followed by a block, as in `if {} else {}`. This else block must be diverging as the outer context cannot be guaranteed to continue soundly without assignment, and no alternate assignment syntax is provided. + ## Alternatives -While this feature can effectively be covered by functions such `or_or`/`ok_or_else` on the `Option` and `Result` types combined with the Try operator (`?`), +While this feature can partly be covered by functions such `or_or`/`ok_or_else` on the `Option` and `Result` types combined with the Try operator (`?`), such functions do not exist automatically on custom enum types and require non-obvious and non-trivial implementation, and may not be map-able to `Option`/`Result`-style functions at all (especially for enums where the "success" variant is contextual and there are many variants). +These functions will also not work for code which wishes to return something other than `Option` or `Result`. ### `unless let ... {}` / `try let ... {}` @@ -298,9 +300,25 @@ An often proposed alternative is to add an extra keyword to the beginning of the One possible benefit of adding a keyword is that it could make a possible future extension for similarity to the (yet unimplemented) [if-let-chains][] feature more straightforward. However, as mentioned in the [future-possibilities][] section, this is likely not necessary. +One drawback of this alternative syntax: it would introduce a binding without either starting a new block containing that binding or starting with a `let`. +Currently, in Rust, only a `let` statement can introduce a binding *in the current block* without starting a new block. +(Note that `static` and `const` are only available outside of block scope.) +This alternative syntax would potentially make it more difficult for Rust developers to scan their code for bindings, as they would need to look for both `let` and `unless let`. +By contrast, a let-else statement begins with `let` and the start of a let-else statement looks exactly like a normal let binding. + This syntax has prior art in the Swift programming language, which includes a [guard-let-else][swift] statement which is roughly equivalent to this proposal except for the choice of keywords. +### `if !let PAT = EXPR { BODY }` + +The [old RFC][old-rfc] originally proposed this general feature via some kind of pattern negation as `if !let PAT = EXPR { BODY }`. + +This RFC avoids adding any kind of new or special pattern matching rules. The pattern matching works as it does for if-let. +The general consensus in the old RFC was also that the negation syntax is much less clear than `if PATTERN = EXPR_WITHOUT_BLOCK else { /* diverge */ };`, +and partway through that RFC's lifecycle it was updated to be similar to this RFC's proposed let-else syntax. + +The `if !let` alternative syntax would also share the binding drawback of the `unless let` alternative syntax. + ### `let PATTERN = EXPR else return EXPR;` A potential alternative to requiring parentheses in `let PATTERN = (if { a } else { b }) else { c };` is to change the syntax of the `else` to no longer be a block @@ -350,15 +368,7 @@ let AnEnum::Variant1(a) = x else assign a { }; ``` -### `if !let PAT = EXPR { BODY }` - -The [old RFC][old-rfc] originally proposed this general feature via some kind of pattern negation as `if !let PAT = EXPR { BODY }`. - -This RFC avoids adding any kind of new or special pattern matching rules. The pattern matching works as it does for if-let. -The general consensus in the old RFC was also that the negation syntax is much less clear than `if PATTERN = EXPR_WITHOUT_BLOCK else { /* diverge */ };`, -and partway through that RFC's lifecycle it was updated to be similar to this RFC's proposed let-else syntax. - -### Complete Alternative +### Null Alternative Don't make any changes; use existing syntax like `match` (or `if let`) as shown in the motivating example, or write macros to simplify the code. @@ -424,7 +434,7 @@ let b = false; // The RFC proposes boolean matches like this be either: // - Made into a compile error, or -// - Made to be parsed internally like if-let-chains: `(let true = a) && b else { ... };` +// - Made to be parsed internally like if-let-chains: `((let true = a) && b) else { ... };` let true = a && b else { return; }; From 22e84304bd2bdb7a20adcf987f437d07c454b483 Mon Sep 17 00:00:00 2001 From: Jeremiah Senkpiel Date: Fri, 4 Jun 2021 08:42:50 -0700 Subject: [PATCH 10/25] let-else: note alternative 'else' names --- text/0000-let-else.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/text/0000-let-else.md b/text/0000-let-else.md index 0e7cc47e0e3..bedf4f8253d 100644 --- a/text/0000-let-else.md +++ b/text/0000-let-else.md @@ -293,9 +293,18 @@ such functions do not exist automatically on custom enum types and require non-o to `Option`/`Result`-style functions at all (especially for enums where the "success" variant is contextual and there are many variants). These functions will also not work for code which wishes to return something other than `Option` or `Result`. +### Naming of `else` (`let ... unless { ... }`) + +One often proposed alternative is to use a different keyword than `else`. +This is supposed to help disambiguate let-else statements from other code with blocks and `else`. + +This RFC avoids this as it would mean loosing symmetry with if-else & if-let-else, +and would require adding a new keyword. +Adding a new keyword could mean more to teach and could promote even more special casing around let-else's semantics. + ### `unless let ... {}` / `try let ... {}` -An often proposed alternative is to add an extra keyword to the beginning of the let-else statement, to denote that it is different than a regular `let` statement. +Another often proposed alternative is to add an extra keyword to the beginning of the let-else statement, to denote that it is different than a regular `let` statement. One possible benefit of adding a keyword is that it could make a possible future extension for similarity to the (yet unimplemented) [if-let-chains][] feature more straightforward. However, as mentioned in the [future-possibilities][] section, this is likely not necessary. From d25d51a1c6724ea4b1fc82e4a1d7f8e46c3d6404 Mon Sep 17 00:00:00 2001 From: Jeremiah Senkpiel Date: Fri, 4 Jun 2021 08:44:37 -0700 Subject: [PATCH 11/25] let-else: unless -> otherwise --- text/0000-let-else.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/text/0000-let-else.md b/text/0000-let-else.md index bedf4f8253d..4f50bd93388 100644 --- a/text/0000-let-else.md +++ b/text/0000-let-else.md @@ -293,9 +293,9 @@ such functions do not exist automatically on custom enum types and require non-o to `Option`/`Result`-style functions at all (especially for enums where the "success" variant is contextual and there are many variants). These functions will also not work for code which wishes to return something other than `Option` or `Result`. -### Naming of `else` (`let ... unless { ... }`) +### Naming of `else` (`let ... otherwise { ... }`) -One often proposed alternative is to use a different keyword than `else`. +One often proposed alternative is to use a different keyword than `else`, such as `otherwise`. This is supposed to help disambiguate let-else statements from other code with blocks and `else`. This RFC avoids this as it would mean loosing symmetry with if-else & if-let-else, From f3473f68686b6e0d397ceb51240ebf2cbbc2471e Mon Sep 17 00:00:00 2001 From: Jeremiah Senkpiel Date: Mon, 7 Jun 2021 17:46:37 -0700 Subject: [PATCH 12/25] let-else, multiple updates - Example now shows side-effects and dependence, cannot be reproduced by a single large `match`. - Note lazy boolean operator dis-allowance more prominently - Note `irrefutable_let_patterns` lint warning. - Make a decision for the if-let-chains future possibility: just disallow the problematic case - Clarify the note about `static` and `const` _items_. - Add section about "let-else within if-let" (I guess this RFC had it coming for it) --- text/0000-let-else.md | 80 +++++++++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 26 deletions(-) diff --git a/text/0000-let-else.md b/text/0000-let-else.md index 4f50bd93388..dfb321e77b8 100644 --- a/text/0000-let-else.md +++ b/text/0000-let-else.md @@ -38,55 +38,70 @@ which require intermediary bindings (usually of the same name). The following two code examples are possible options with current Rust code. ```rust -if let Some(a) = x { - if let Some(b) = y { - if let Some(c) = z { +if let Some(x) = xyz { + if let Some(y) = x.thing() { + if let Some(z) = y.thing() { // ... - do_something_with(a, b, c); + do_something_with(z); // ... } else { + info!("z was bad"); return Err("bad z"); } } else { + info!("y was bad"); return Err("bad y"); } } else { + info!("x was bad"); return Err("bad x"); } ``` ```rust -let a = match x { - Some(a) => a, - _ => return Err("bad x"), +let x = match xyz { + Some(x) => x, + _ => { + info!("x was bad"); + return Err("bad x") + }, }; -let b = match y { - Some(b) => b, - _ => return Err("bad y"), +let y = match x.thing() { + Some(y) => y, + _ => { + info!("y was bad"); + return Err("bad y") + }, }; -let c = match z { - Some(c) => c, - _ => return Err("bad z"), +let z = match y.thing() { + Some(z) => z, + _ => return { + info!("z was bad"); + Err("bad z") + }, }; // ... -do_something_with(a, b, c); +do_something_with(z); // ... ``` Both of the above examples would be able to be written as: ```rust -let Some(a) = x else { +let Some(x) = xyz else { + info!("x was bad"); return Err("bad x"); }; -let Some(b) = y else { +let Some(y) = x.thing() else { + info!("y was bad"); return Err("bad y"); }; -let Some(c) = z else { +let Some(z) = y.thing() else { + info!("z was bad"); return Err("bad z"); }; // ... -do_something_with(a, b, c); +do_something_with(z); // ... ``` @@ -222,7 +237,10 @@ Any expression may be put into the expression position except an `if {} else {}` While `if {} else {}` is technically feasible this RFC proposes it be disallowed for programmer clarity to avoid an `... else {} else {}` situation. Rust already provides us with such a restriction, [`ExpressionWithoutBlock`][expressions]. -Any pattern that could be put into if-let's pattern position can be put into let-else's pattern position. +Any pattern that could be put into if-let's pattern position can be put into let-else's pattern position, except a match to a boolean when +a lazy boolean operator is present (`&&` or `||`), for reasons noted in [future-possibilities][]. + +If the pattern is irrefutable, rustc will emit the `irrefutable_let_patterns` warning lint, as it does with an irrefutable pattern in an `if let`. The `else` block must diverge. This could be a keyword which diverges (returns `!`), or a panic. This likely necessitates a new subtype of `BlockExpression`, something like `BlockExpressionDiverging`. @@ -273,9 +291,8 @@ Specifically, while the following example is an error today, by the default `&&` let a = false; let b = false; -// The RFC proposes boolean matches like this be either: -// - Made into a compile error, or -// - Made to be parsed internally like if-let-chains: `((let true = a) && b) else { ... };` +// The RFC proposes boolean patterns with a lazy boolean operator (&& or ||) +// be made into a compile error, for potential future compatibility with if-let-chains. let true = a && b else { return; }; @@ -311,7 +328,7 @@ However, as mentioned in the [future-possibilities][] section, this is likely no One drawback of this alternative syntax: it would introduce a binding without either starting a new block containing that binding or starting with a `let`. Currently, in Rust, only a `let` statement can introduce a binding *in the current block* without starting a new block. -(Note that `static` and `const` are only available outside of block scope.) +(Note that [`static`][] and [`const`][] are _items_, which can be forward-referenced.) This alternative syntax would potentially make it more difficult for Rust developers to scan their code for bindings, as they would need to look for both `let` and `unless let`. By contrast, a let-else statement begins with `let` and the start of a let-else statement looks exactly like a normal let binding. @@ -441,9 +458,8 @@ let Some(x) = maybe && has_thing else { let a = false; let b = false; -// The RFC proposes boolean matches like this be either: -// - Made into a compile error, or -// - Made to be parsed internally like if-let-chains: `((let true = a) && b) else { ... };` +// The RFC proposes boolean patterns with a lazy boolean operator (&& or ||) +// be made into a compile error, for potential future compatibility with if-let-chains. let true = a && b else { return; }; @@ -485,6 +501,18 @@ let Ok(a) = x else match { } ``` +## let-else within if-let + +Conceivable. This RFC makes no judgement, even if the author does. + +```rust +if let Some(x) = y else { return; } { + // I guess this RFC had it coming for it +} +``` + +[`const`]: https://doc.rust-lang.org/reference/items/constant-items.html +[`static`]: https://doc.rust-lang.org/reference/items/static-items.html [expressions]: https://doc.rust-lang.org/reference/expressions.html#expressions [old-rfc]: https://github.com/rust-lang/rfcs/pull/1303 [if-let]: https://rust-lang.github.io/rfcs/0160-if-let.html From 6a9a9efab68de965bfb0885cee612cd42bf3a8d1 Mon Sep 17 00:00:00 2001 From: Jeremiah Senkpiel Date: Mon, 14 Jun 2021 12:15:11 -0700 Subject: [PATCH 13/25] let-else, another round of updates - Clarify the diverging return type to be anything which returns never. - More detailed desugaring example with the above point. - Get rid of `BlockExpressionDiverging`. - Alternative: `let` assignment from `match`. - Alternative: `||` in pattern matching. - Prior art: `guard!` macro. --- text/0000-let-else.md | 104 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 93 insertions(+), 11 deletions(-) diff --git a/text/0000-let-else.md b/text/0000-let-else.md index dfb321e77b8..f9436933504 100644 --- a/text/0000-let-else.md +++ b/text/0000-let-else.md @@ -242,16 +242,30 @@ a lazy boolean operator is present (`&&` or `||`), for reasons noted in [future- If the pattern is irrefutable, rustc will emit the `irrefutable_let_patterns` warning lint, as it does with an irrefutable pattern in an `if let`. -The `else` block must diverge. This could be a keyword which diverges (returns `!`), or a panic. -This likely necessitates a new subtype of `BlockExpression`, something like `BlockExpressionDiverging`. -Allowed keywords: -- `return` -- `break` -- `continue` +The `else` block must _diverge_, meaning the `else` block must return the [never type (`!`)][never type]). +This could be a keyword which diverges (returns `!`), or a panic. If the pattern does not match, the expression is not consumed, and so any existing variables from the surrounding scope are accessible as they would normally be. +## Desugaring example + +```rust +let Some(x) = y else { return; }; +``` + +Desugars to + +```rust +let x = match y { + Some(x) => y, + _ => { + let nope: ! = { return; }; + match nope {} + } +} +``` + # Drawbacks [drawbacks]: #drawbacks @@ -260,7 +274,8 @@ accessible as they would normally be. "Must diverge" is an unusual requirement, which doesn't exist elsewhere in the language as of the time of writing, and might be difficult to explain or lead to confusing errors for programmers new to this feature. -This also necessitates a new block expression subtype, something like `BlockExpressionDiverging`. +However, rustc does have support for representing the divergence through the type-checker via `!` or any other uninhabitable type, +so the implementation is not a problem. ## `let PATTERN = if {} else {} else {};` @@ -315,8 +330,7 @@ These functions will also not work for code which wishes to return something oth One often proposed alternative is to use a different keyword than `else`, such as `otherwise`. This is supposed to help disambiguate let-else statements from other code with blocks and `else`. -This RFC avoids this as it would mean loosing symmetry with if-else & if-let-else, -and would require adding a new keyword. +This RFC avoids this as it would mean losing symmetry with if-else and if-let-else, and would require adding a new keyword. Adding a new keyword could mean more to teach and could promote even more special casing around let-else's semantics. ### `unless let ... {}` / `try let ... {}` @@ -394,6 +408,67 @@ let AnEnum::Variant1(a) = x else assign a { }; ``` +### Assign to outer scope from `match` + +Another alternative is to allow assigning to the outer scope from within a `match`. + +```rust +match thing { + Happy(x) => let x, // Assigns x to outer scope. + Sad(y) => return Err(format!("We were sad because of {}", y)), + Tragic(z) => return Err(format!("We cried hard because of {}", z)), +} +``` + +However this is not an obvious opposite io if-let, and would introduce an entirely new positional meaning of `let`. + +### `||` in pattern-matching + +A more complex, more flexible, but less obvious alternative is to allow `||` in any pattern matches as a fall-through match case fallback. +Such a feature would likely interact more directly with [if-let-chains][], but could also be use to allow refutable patterns in let statements +by covering every possible variant of an enum (possibly by use of a diverging fallback block similar to `_` in `match`). + +For example, covering the use-case of let-else: +```rust +let Some(x) = a || { return; }; +``` + +With a fallback: +```rust +let Some(x) = a || b || { return; }; +``` + +Combined with `&&` as proposed in if-let-chains, constructs such as the following are conceivable: + +```rust +let Enum::Var1(x) = a || b || { return anyhow!("Bad x"); } && let Some(z) = x || y; +// Complex. Both x and z are now in scope. +``` + +This is not a simple construct, and could be quite confusing to newcomers + +That being said, such a thing would be very non-trivial to write today, and might be just as confusing to read: +```rust +let x = match a { + Enum::Var1(x) => x, + _ => { + match b { + Enum::Var1(x) => x, + _ => { + return anyhow!("Bad x"); + }, + } + } +}; +let z = match x { + Some(z) => z, + _ => y, +}; +// Complex. Both x and z are now in scope. +``` + +This is, as stated, a much more complex alternative interacting with much more of the language, and is also not an obvious opposite of if-let expressions. + ### Null Alternative Don't make any changes; use existing syntax like `match` (or `if let`) as shown in the motivating example, or write macros to simplify the code. @@ -409,6 +484,10 @@ The Swift programming language, which inspired Rust's if-let expression, also includes a [guard-let-else][swift] statement which is roughly equivalent to this proposal except for the choice of keywords. +A `guard!` macro implementing something very similar to this RFC has been available on crates.io since 2015 (the time of the old RFC). +- [Crate for `guard!`][guard-crate] +- [GitHub repo for `guard!`][guard-repo] + The `match` alternative in particular is fairly prevalent in rust code on projects which have many possible error conditions. The Try operator allows for an `ok_or_else` alternative to be used where the types are only `Option` and `Result`, @@ -441,7 +520,7 @@ let Some(foo) = (expr() && foo.is_baz() && let Ok(yay) = qux(foo) else { ... }) ``` However, given that all existing occurrences of this behavior before this RFC are type errors anyways, -a specific boolean-only case can be avoided and thus parsing can be changed to lave the door open to this possible extension. +a specific boolean-only case can be avoided and thus parsing can be changed to leave the door open to this possible extension. This boolean case is always equivalent to a less flexible `if` statement and as such is not useful. ```rust @@ -514,7 +593,10 @@ if let Some(x) = y else { return; } { [`const`]: https://doc.rust-lang.org/reference/items/constant-items.html [`static`]: https://doc.rust-lang.org/reference/items/static-items.html [expressions]: https://doc.rust-lang.org/reference/expressions.html#expressions -[old-rfc]: https://github.com/rust-lang/rfcs/pull/1303 +[guard-crate]: https://crates.io/crates/guard +[guard-repo]: https://github.com/durka/guard [if-let]: https://rust-lang.github.io/rfcs/0160-if-let.html [if-let-chains]: https://rust-lang.github.io/rfcs/2497-if-let-chains.html +[never-type]: https://doc.rust-lang.org/std/primitive.never.html +[old-rfc]: https://github.com/rust-lang/rfcs/pull/1303 [swift]: https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/ControlFlow.html#//apple_ref/doc/uid/TP40014097-CH9-ID525 From 18ebdca55b2c01d80c9b09718a8ec4c98bb65a49 Mon Sep 17 00:00:00 2001 From: Jeremiah Senkpiel Date: Thu, 17 Jun 2021 16:06:30 -0700 Subject: [PATCH 14/25] let-else, avoid let-else within if-let per Josh's recommendation and general discussion --- text/0000-let-else.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/text/0000-let-else.md b/text/0000-let-else.md index f9436933504..b1774b4f469 100644 --- a/text/0000-let-else.md +++ b/text/0000-let-else.md @@ -248,6 +248,9 @@ This could be a keyword which diverges (returns `!`), or a panic. If the pattern does not match, the expression is not consumed, and so any existing variables from the surrounding scope are accessible as they would normally be. +let-else does not combine with the `let` from if-let, as if-let is not actually a _let statement_. +If you ever try to write something like `if let p = e else { } { }`, instead use a regular if-else by writing `if let p = e { } else { }`. + ## Desugaring example ```rust @@ -582,7 +585,8 @@ let Ok(a) = x else match { ## let-else within if-let -Conceivable. This RFC makes no judgement, even if the author does. +This RFC naturally brings with it the question of if let-else should be allowable in the `let` position within if-let, +creating a potentially confusing and poorly reading construct: ```rust if let Some(x) = y else { return; } { @@ -590,6 +594,10 @@ if let Some(x) = y else { return; } { } ``` +However, since the `let` within if-let is part of the if-let expression and is not an actual `let` statement, this would have to be +explicitly allowed. This RFC does not propose we allow this. Rather, rust should avoid ever allowing this, +because it is confusing to read syntactically, and it is functionally similar to `if let p = e { } else { }` but with more drawbacks. + [`const`]: https://doc.rust-lang.org/reference/items/constant-items.html [`static`]: https://doc.rust-lang.org/reference/items/static-items.html [expressions]: https://doc.rust-lang.org/reference/expressions.html#expressions From 324117ca3398125d68a0aae37b9c9c983629364c Mon Sep 17 00:00:00 2001 From: Jeremiah Senkpiel Date: Thu, 17 Jun 2021 16:39:26 -0700 Subject: [PATCH 15/25] let-else, add note about multiple patterns --- text/0000-let-else.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/text/0000-let-else.md b/text/0000-let-else.md index b1774b4f469..b9a896437cd 100644 --- a/text/0000-let-else.md +++ b/text/0000-let-else.md @@ -248,6 +248,12 @@ This could be a keyword which diverges (returns `!`), or a panic. If the pattern does not match, the expression is not consumed, and so any existing variables from the surrounding scope are accessible as they would normally be. +For patterns which match multiple variants, such as through the `|` (or) syntax, all variants must produce the same bindings (ignoring additional bindings in uneven patterns), +and those bindings must all be names the same. Valid example: +```rust +let Some(x) | MyEnum::VarientA(_, _, x) | MyEnum::VarientB { x, .. } = a else { return; }; +``` + let-else does not combine with the `let` from if-let, as if-let is not actually a _let statement_. If you ever try to write something like `if let p = e else { } { }`, instead use a regular if-else by writing `if let p = e { } else { }`. From bb81a98fbb39b2e7b0c0b3d1ab084bbd9cfbeb54 Mon Sep 17 00:00:00 2001 From: Jeremiah Senkpiel Date: Thu, 17 Jun 2021 16:50:33 -0700 Subject: [PATCH 16/25] let-else, note let-else-else-chains Most of this was Josh's suggestion and some is his words almost verbatim. Co-Authored-By: Josh Triplett --- text/0000-let-else.md | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/text/0000-let-else.md b/text/0000-let-else.md index b9a896437cd..5d79ff84cec 100644 --- a/text/0000-let-else.md +++ b/text/0000-let-else.md @@ -404,7 +404,7 @@ let AnEnum::Variant1(a) = x else { }; ``` -Another potential alternative for fall-back which could be added with an additional keyword as a future extension: +Another potential alternative for fall-back: ```rust enum AnEnum { @@ -412,9 +412,9 @@ enum AnEnum { Variant2(String), } -let AnEnum::Variant1(a) = x else assign a { - a = 42; -}; +let Ok(a) = x else match { + Err(e) => return Err(e.into()), +} ``` ### Assign to outer scope from `match` @@ -568,20 +568,31 @@ error[E0005]: refutable pattern in local binding: `false` not covered This RFC does not suggest that we do any of these, but notes that they would be future possibilities. -If fall-back assignment as discussed above in [rationale-and-alternatives][] is desirable, it could be added with an additional keyword as a future extension: +If fall-back assignment as discussed above in [rationale-and-alternatives][] is desirable, it could be added a few different ways, +not all potential ways are covered here, but the ones which seem most popular at time of writing are: + +### let-else-else-chains + +Where the pattern is sequentially matched against each expression following an else, up until a required diverging block if the pattern did not match on any value. +Similar to the above-mentioned alternative of `||` in pattern-matching, but restricted to only be used with let-else. ```rust -enum AnEnum { - Variant1(u32), - Variant2(String), -} +let Some(x) = a else b else c else { return; }; +``` -let AnEnum::Variant1(a) = x else assign a { - a = 42; -}; +Another way to look at let-else-else-chains: a `match` statement takes one expression and applies multiple patterns to it until one matches, +while let-else-else-chains would take one pattern and apply it to multiple expressions until one matches. + +This has a complexity issue with or-patterns, where expressions can _easily_ become exponential. +(This is already possible with or-patterns with guards but this would make it much easier to encounter.) + +```rust +let A(x) | B(x) = foo() else bar() else { return; }; ``` -Another potential form of the fall-back extension: +### let-else-match + +Where the `match` must cover all patters which are not the let assignment pattern. ```rust let Ok(a) = x else match { From ff866ead2285eab6c762b0a9cd7dc9766b13144b Mon Sep 17 00:00:00 2001 From: Jeremiah Senkpiel Date: Thu, 17 Jun 2021 16:50:49 -0700 Subject: [PATCH 17/25] let-else, note that `||` in patterns is still possible --- text/0000-let-else.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/text/0000-let-else.md b/text/0000-let-else.md index 5d79ff84cec..af055463f1e 100644 --- a/text/0000-let-else.md +++ b/text/0000-let-else.md @@ -600,6 +600,15 @@ let Ok(a) = x else match { } ``` +## `||` in pattern-matching + +A variant of `||` in pattern-matching could still be a non-conflicting addition if it was allowed to be refutable, ending up with constructs similar to the +above mentioned let-else-else-chains. In this way it would add to let-else rather than replace it. + +```rust +let Some(x) = a || b else { return; }; +``` + ## let-else within if-let This RFC naturally brings with it the question of if let-else should be allowable in the `let` position within if-let, From a4d945650ccf4f7f9ba4dc76ed7c72b862539e0e Mon Sep 17 00:00:00 2001 From: Jeremiah Senkpiel Date: Thu, 17 Jun 2021 16:53:52 -0700 Subject: [PATCH 18/25] let-else, update || example desugar Thanks to lebensterben for catching this one --- text/0000-let-else.md | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/text/0000-let-else.md b/text/0000-let-else.md index af055463f1e..fd393d0906e 100644 --- a/text/0000-let-else.md +++ b/text/0000-let-else.md @@ -456,18 +456,14 @@ let Enum::Var1(x) = a || b || { return anyhow!("Bad x"); } && let Some(z) = x || This is not a simple construct, and could be quite confusing to newcomers -That being said, such a thing would be very non-trivial to write today, and might be just as confusing to read: +That being said, such a thing is not perfectly obvious to write today, and might be just as confusing to read: ```rust -let x = match a { - Enum::Var1(x) => x, - _ => { - match b { - Enum::Var1(x) => x, - _ => { - return anyhow!("Bad x"); - }, - } - } +let x = if let Enum::Var1(v) = a { + v +} else if let Enum::Var1(v) = b { + v +} else { + anyhow!("Bad x") }; let z = match x { Some(z) => z, From cab12fc5959fa4ceb12721ef47f6f1fb2363efd2 Mon Sep 17 00:00:00 2001 From: Jeremiah Senkpiel Date: Thu, 17 Jun 2021 16:58:14 -0700 Subject: [PATCH 19/25] let-else, variable name spelling long sigh --- text/0000-let-else.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-let-else.md b/text/0000-let-else.md index fd393d0906e..3d56e110ca2 100644 --- a/text/0000-let-else.md +++ b/text/0000-let-else.md @@ -251,7 +251,7 @@ accessible as they would normally be. For patterns which match multiple variants, such as through the `|` (or) syntax, all variants must produce the same bindings (ignoring additional bindings in uneven patterns), and those bindings must all be names the same. Valid example: ```rust -let Some(x) | MyEnum::VarientA(_, _, x) | MyEnum::VarientB { x, .. } = a else { return; }; +let Some(x) | MyEnum::VariantA(_, _, x) | MyEnum::VariantB { x, .. } = a else { return; }; ``` let-else does not combine with the `let` from if-let, as if-let is not actually a _let statement_. From 89c5b6e13e2b023ac22bd0fc924a8a9921e569e9 Mon Sep 17 00:00:00 2001 From: Jeremiah Senkpiel Date: Mon, 28 Jun 2021 16:19:11 -0700 Subject: [PATCH 20/25] let-else, PFCP updates - Clarify that `: TYPE` is allowed, as per Josh - Note the expression restriction more accurately and with more detail, as per Niko - Removed the option-chaining example, because many people keep stopping at that to comment. - Note a macro as an (unfavorable) alternative. --- text/0000-let-else.md | 141 +++++++++++++++++++----------------------- 1 file changed, 65 insertions(+), 76 deletions(-) diff --git a/text/0000-let-else.md b/text/0000-let-else.md index 3d56e110ca2..aa9dedd97ce 100644 --- a/text/0000-let-else.md +++ b/text/0000-let-else.md @@ -6,12 +6,13 @@ # Summary [summary]: #summary -Introduce a new `let PATTERN = EXPRESSION_WITHOUT_BLOCK else DIVERGING_BLOCK;` construct (informally called a +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)][old-rfc] for an almost identical feature. @@ -35,87 +36,60 @@ which require intermediary bindings (usually of the same name). ## Examples -The following two code examples are possible options with current Rust code. +let-else is particularly useful when dealing with enums which are not `Option`/`Result`, and as such do not have access to e.g. `ok_or()`. +Consider the following example transposed from a real-world project written in part by the author: +Without let-else, as this code was originally written: ```rust -if let Some(x) = xyz { - if let Some(y) = x.thing() { - if let Some(z) = y.thing() { - // ... - do_something_with(z); - // ... +impl ActionView { + pub(crate) fn new(history: &History) -> Result { + let mut iter = history.iter(); + let event = iter + .next() + // RFC comment: ok_or_else works fine to early return when working with `Option`. + .ok_or_else(|| eyre::eyre!("Entity has no history"))?; + + if let Action::Register { + actor: String, + x: Vec + y: u32, + z: String, + } = event.action().clone() { + let created = *event.created(); + let mut view = ActionView { + registered_by: (actor, created), + a: (actor.clone(), x, created), + b: (actor.clone(), y, created), + c: (z, created), + d: Vec::new(), + + e: None, + f: None, + g: None, + }; + for event in iter { + view.update(&event)?; + } + + // more lines omitted + + Ok(view) } else { - info!("z was bad"); - return Err("bad z"); + // RFC comment: Far away from the associated conditional. + Err(eyre::eyre!("must begin with a Register action")); } - } else { - info!("y was bad"); - return Err("bad y"); } -} else { - info!("x was bad"); - return Err("bad x"); } ``` -```rust -let x = match xyz { - Some(x) => x, - _ => { - info!("x was bad"); - return Err("bad x") - }, -}; -let y = match x.thing() { - Some(y) => y, - _ => { - info!("y was bad"); - return Err("bad y") - }, -}; -let z = match y.thing() { - Some(z) => z, - _ => return { - info!("z was bad"); - Err("bad z") - }, -}; -// ... -do_something_with(z); -// ... -``` - -Both of the above examples would be able to be written as: - -```rust -let Some(x) = xyz else { - info!("x was bad"); - return Err("bad x"); -}; -let Some(y) = x.thing() else { - info!("y was bad"); - return Err("bad y"); -}; -let Some(z) = y.thing() else { - info!("z was bad"); - return Err("bad z"); -}; -// ... -do_something_with(z); -// ... -``` - -which succinctly avoids bindings of the same name, rightward shift, etc. - -let-else is even more useful when dealing with enums which are not `Option`/`Result`, consider how the -following code would look without let-else (transposed from a real-world project written in part by the author): - +With let-else: ```rust impl ActionView { pub(crate) fn new(history: &History) -> Result { let mut iter = history.iter(); let event = iter .next() + // RFC comment: ok_or_else works fine to early return when working with `Option`. .ok_or_else(|| eyre::eyre!("Entity has no history"))?; let Action::Register { @@ -124,9 +98,7 @@ impl ActionView { y: u32, z: String, } = event.action().clone() else { - // RFC Author's note: - // Without if-else this was separated from the conditional - // by a substantial block of code which now follows below. + // RFC comment: Directly located next to the associated conditional. return Err(eyre::eyre!("must begin with a Register action")); }; @@ -145,6 +117,9 @@ impl ActionView { for event in iter { view.update(&event)?; } + + // more lines omitted + Ok(view) } } @@ -233,12 +208,17 @@ let (each, binding) = match expr { }; ``` -Any expression may be put into the expression position except an `if {} else {}` as explain below in [drawbacks][]. -While `if {} else {}` is technically feasible this RFC proposes it be disallowed for programmer clarity to avoid an `... else {} else {}` situation. -Rust already provides us with such a restriction, [`ExpressionWithoutBlock`][expressions]. +Most expressions may be put into the expression position with two restrictions: +1. May not include a block outside of parenthesis. (Must be an [`ExpressionWithoutBlock`][expressions].) +2. May not be just a lazy boolean expression (`&&` or `||`). (Must not be a [`LazyBooleanExpression`][lazy-boolean-operators].) -Any pattern that could be put into if-let's pattern position can be put into let-else's pattern position, except a match to a boolean when -a lazy boolean operator is present (`&&` or `||`), for reasons noted in [future-possibilities][]. +While allowing e.g. `if {} else {}` directly in the expression position is technically feasible this RFC proposes it be +disallowed for programmer clarity so as to avoid `... else {} else {}` situations as discussed in the [drawbacks][] section. +Boolean matches are not useful with let-else and so lazy boolean expressions are disallowed for reasons noted in [future-possibilities][]. +These types of expressions can still be used when combined in a less ambiguous manner with parenthesis, thus forming a [`GroupedExpression`][grouped-expr], +which is allowed under the two expression restrictions. + +Any refutable pattern that could be put into if-let's pattern position can be put into let-else's pattern position. If the pattern is irrefutable, rustc will emit the `irrefutable_let_patterns` warning lint, as it does with an irrefutable pattern in an `if let`. @@ -474,6 +454,13 @@ let z = match x { This is, as stated, a much more complex alternative interacting with much more of the language, and is also not an obvious opposite of if-let expressions. +### Macro + +Another suggested solution is to create a macro which handles this. +A crate containing such a macro is mentioned in the [Prior art](prior-art) section of this RFC. + +This crate has not been widely used in the rust crate ecosystem with only 47k downloads over the ~6 years it has existed at the time of writing. + ### Null Alternative Don't make any changes; use existing syntax like `match` (or `if let`) as shown in the motivating example, or write macros to simplify the code. @@ -623,10 +610,12 @@ because it is confusing to read syntactically, and it is functionally similar to [`const`]: https://doc.rust-lang.org/reference/items/constant-items.html [`static`]: https://doc.rust-lang.org/reference/items/static-items.html [expressions]: https://doc.rust-lang.org/reference/expressions.html#expressions +[grouped-expr]: https://doc.rust-lang.org/reference/expressions/grouped-expr.html [guard-crate]: https://crates.io/crates/guard [guard-repo]: https://github.com/durka/guard [if-let]: https://rust-lang.github.io/rfcs/0160-if-let.html [if-let-chains]: https://rust-lang.github.io/rfcs/2497-if-let-chains.html +[lazy-boolean-operators]: https://doc.rust-lang.org/reference/expressions/operator-expr.html#lazy-boolean-operators [never-type]: https://doc.rust-lang.org/std/primitive.never.html [old-rfc]: https://github.com/rust-lang/rfcs/pull/1303 [swift]: https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/ControlFlow.html#//apple_ref/doc/uid/TP40014097-CH9-ID525 From 54bca552f047a87b6842d00996bcbd428cd750d7 Mon Sep 17 00:00:00 2001 From: Jeremiah Senkpiel Date: Fri, 2 Jul 2021 14:04:26 -0700 Subject: [PATCH 21/25] let-else, fix a never type link Co-authored-by: J. Frimmel <31166235+jfrimmel@users.noreply.github.com> --- text/0000-let-else.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-let-else.md b/text/0000-let-else.md index aa9dedd97ce..a1e00c3332c 100644 --- a/text/0000-let-else.md +++ b/text/0000-let-else.md @@ -222,7 +222,7 @@ Any refutable pattern that could be put into if-let's pattern position can be pu If the pattern is irrefutable, rustc will emit the `irrefutable_let_patterns` warning lint, as it does with an irrefutable pattern in an `if let`. -The `else` block must _diverge_, meaning the `else` block must return the [never type (`!`)][never type]). +The `else` block must _diverge_, meaning the `else` block must return the [never type (`!`)][never-type]). This could be a keyword which diverges (returns `!`), or a panic. If the pattern does not match, the expression is not consumed, and so any existing variables from the surrounding scope are From c0c2fccc15c7812f182d8e042270086514b11278 Mon Sep 17 00:00:00 2001 From: Jeremiah Senkpiel Date: Tue, 13 Jul 2021 16:46:19 -0700 Subject: [PATCH 22/25] let-else, FCP updates Hopefully this will be my last round of updates. - Added some unresolved questions. - Fixed some typos / english language clarification. - Clarified that all expressions ending in `}` should be disallowed. - Fixed some errors in examples. - Added section on `, else`. - Updated the `unless let` section to be "Introducer syntax" with the noted keyword being `guard`. - Noted the `DIVERGING_EXPR` section with more detail. --- text/0000-let-else.md | 73 ++++++++++++++++++++++++++++++------------- 1 file changed, 52 insertions(+), 21 deletions(-) diff --git a/text/0000-let-else.md b/text/0000-let-else.md index a1e00c3332c..8bd468055f9 100644 --- a/text/0000-let-else.md +++ b/text/0000-let-else.md @@ -12,7 +12,7 @@ Introduce a new `let PATTERN: TYPE = EXPRESSION else DIVERGING_BLOCK;` construct 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`. +The expression has some restrictions, notably it may not end with an `}` or be just a `LazyBooleanExpression`. This RFC is a modernization of a [2015 RFC (pull request 1303)][old-rfc] for an almost identical feature. @@ -151,7 +151,7 @@ let features = match geojson { }; ``` -However, with if-let this could be more succinct & clear: +However, with let-else this could be more succinct & clear: ```rust let GeoJson::FeatureCollection(features) = geojson else { @@ -209,7 +209,9 @@ let (each, binding) = match expr { ``` Most expressions may be put into the expression position with two restrictions: -1. May not include a block outside of parenthesis. (Must be an [`ExpressionWithoutBlock`][expressions].) +1. May not include a block outside of parenthesis. + - Must be an [`ExpressionWithoutBlock`][expressions]. + - [`GroupedExpression`][grouped-expr]-s ending with a `}` are additionally not allowed and must be put in parenthesis. 2. May not be just a lazy boolean expression (`&&` or `||`). (Must not be a [`LazyBooleanExpression`][lazy-boolean-operators].) While allowing e.g. `if {} else {}` directly in the expression position is technically feasible this RFC proposes it be @@ -231,7 +233,7 @@ accessible as they would normally be. For patterns which match multiple variants, such as through the `|` (or) syntax, all variants must produce the same bindings (ignoring additional bindings in uneven patterns), and those bindings must all be names the same. Valid example: ```rust -let Some(x) | MyEnum::VariantA(_, _, x) | MyEnum::VariantB { x, .. } = a else { return; }; +let MyEnum::VariantA(_, _, x) | MyEnum::VariantB { x, .. } = a else { return; }; ``` let-else does not combine with the `let` from if-let, as if-let is not actually a _let statement_. @@ -247,7 +249,7 @@ Desugars to ```rust let x = match y { - Some(x) => y, + Some(x) => x, _ => { let nope: ! = { return; }; match nope {} @@ -263,7 +265,7 @@ let x = match y { "Must diverge" is an unusual requirement, which doesn't exist elsewhere in the language as of the time of writing, and might be difficult to explain or lead to confusing errors for programmers new to this feature. -However, rustc does have support for representing the divergence through the type-checker via `!` or any other uninhabitable type, +However, rustc does have support for representing the divergence through the type-checker via `!` or any other uninhabited type, so the implementation is not a problem. ## `let PATTERN = if {} else {} else {};` @@ -322,9 +324,17 @@ This is supposed to help disambiguate let-else statements from other code with b This RFC avoids this as it would mean losing symmetry with if-else and if-let-else, and would require adding a new keyword. Adding a new keyword could mean more to teach and could promote even more special casing around let-else's semantics. -### `unless let ... {}` / `try let ... {}` +### Comma-before-else (`, else { ... }`) -Another often proposed alternative is to add an extra keyword to the beginning of the let-else statement, to denote that it is different than a regular `let` statement. +Another proposal very similar to renaming `else` it to have it be proceeded by some character such as a comma. + +It is possible that adding such additional separating syntax would make combinations with expressions which have blocks +easier to read and less ambiguous, but is also generally inconsistent with the rest of the rust language at time of writing. + +### Introducer syntax (`guard let ... {}`) + +Another often proposed alternative is to add some introducer syntax (usually an extra keyword) to the beginning of the let-else statement, +to denote that it is different than a regular `let` statement. One possible benefit of adding a keyword is that it could make a possible future extension for similarity to the (yet unimplemented) [if-let-chains][] feature more straightforward. However, as mentioned in the [future-possibilities][] section, this is likely not necessary. @@ -348,17 +358,18 @@ and partway through that RFC's lifecycle it was updated to be similar to this RF The `if !let` alternative syntax would also share the binding drawback of the `unless let` alternative syntax. -### `let PATTERN = EXPR else return EXPR;` +### `let PATTERN = EXPR else DIVERGING_EXPR;` -A potential alternative to requiring parentheses in `let PATTERN = (if { a } else { b }) else { c };` is to change the syntax of the `else` to no longer be a block -but instead an expression which starts with a diverging keyword, such as `return` or `break`. +A potential alternative to requiring parentheses in `let PATTERN = (if { a } else { b }) else { c };` +is to change the syntax of the `else` to no longer be a block but instead _any_ expression which diverges, +such as a `return`, `break`, or any block which diverges. Example: -``` +```rust let Some(foo) = some_option else return None; ``` -This RFC avoids this because it is overall less consistent with `else` from if-else, which require blocks. +This RFC avoids this because it is overall less consistent with `else` from if-else, which requires block expressions. This was originally suggested in the old RFC, comment at https://github.com/rust-lang/rfcs/pull/1303#issuecomment-188526691 @@ -409,7 +420,7 @@ match thing { } ``` -However this is not an obvious opposite io if-let, and would introduce an entirely new positional meaning of `let`. +However this is not an obvious opposite to if-let, and would introduce an entirely new positional meaning of `let`. ### `||` in pattern-matching @@ -430,13 +441,13 @@ let Some(x) = a || b || { return; }; Combined with `&&` as proposed in if-let-chains, constructs such as the following are conceivable: ```rust -let Enum::Var1(x) = a || b || { return anyhow!("Bad x"); } && let Some(z) = x || y; +let Enum::Var1(x) = a || b || { return anyhow!("Bad x"); } && let Some(z) = x || y || { break; }; // Complex. Both x and z are now in scope. ``` This is not a simple construct, and could be quite confusing to newcomers -That being said, such a thing is not perfectly obvious to write today, and might be just as confusing to read: +That said, such a thing is not perfectly obvious to write today, and might be just as confusing to read: ```rust let x = if let Enum::Var1(v) = a { v @@ -445,9 +456,12 @@ let x = if let Enum::Var1(v) = a { } else { anyhow!("Bad x") }; -let z = match x { - Some(z) => z, - _ => y, +let z = if let Some(v) = x { + v +} else if let Some(v) = y { + v +} else { + break; }; // Complex. Both x and z are now in scope. ``` @@ -488,8 +502,25 @@ which is considered to be idiomatic rust. # Unresolved questions [unresolved-questions]: #unresolved-questions -None known at time of writing due to extensive pre-discussion in Zulip: -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 +## Readability in practice + +Will `let ... else { ... };` be clear enough to humans in practical code, or will some introducer syntax be desirable? + +## Conflicts with if-let-chains + +Does this conflict too much with the if-let-chains RFC or vice-versa? + +Neither this feature nor that feature should be stabilized without considering the other. + +## Amount of special cases + +Are there too many special-case interactions with other features? + +## Grammar clarity + +Does the grammar need to be clarified? + +This RFC has some slightly unusual grammar requirements. # Future possibilities [future-possibilities]: #future-possibilities From 76e8bb5c07f3c960edd50d46d735376a754ec420 Mon Sep 17 00:00:00 2001 From: Jeremiah Senkpiel Date: Mon, 19 Jul 2021 16:51:21 -0700 Subject: [PATCH 23/25] let-else, Mario's edits Excluding the block grammar clarifications for now, because those have evolved somewhat in Zulip. Co-authored-by: Mario Carneiro --- text/0000-let-else.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/text/0000-let-else.md b/text/0000-let-else.md index 8bd468055f9..785e0f01fc6 100644 --- a/text/0000-let-else.md +++ b/text/0000-let-else.md @@ -225,7 +225,7 @@ Any refutable pattern that could be put into if-let's pattern position can be pu If the pattern is irrefutable, rustc will emit the `irrefutable_let_patterns` warning lint, as it does with an irrefutable pattern in an `if let`. The `else` block must _diverge_, meaning the `else` block must return the [never type (`!`)][never-type]). -This could be a keyword which diverges (returns `!`), or a panic. +This could be a keyword which diverges (returns `!`), such as `return`, `break`, `continue` or `loop { ... }`, a diverging function like `std::process::abort` or `std::process::exit`, or a panic. If the pattern does not match, the expression is not consumed, and so any existing variables from the surrounding scope are accessible as they would normally be. @@ -315,6 +315,8 @@ While this feature can partly be covered by functions such `or_or`/`ok_or_else` such functions do not exist automatically on custom enum types and require non-obvious and non-trivial implementation, and may not be map-able to `Option`/`Result`-style functions at all (especially for enums where the "success" variant is contextual and there are many variants). These functions will also not work for code which wishes to return something other than `Option` or `Result`. +Moreover, this does not cover diverging blocks that do something other than return with an error or target an enclosing `try` block, +for example if the diverging expression is `continue e` or `break 'outer_loop e`. ### Naming of `else` (`let ... otherwise { ... }`) @@ -445,7 +447,7 @@ let Enum::Var1(x) = a || b || { return anyhow!("Bad x"); } && let Some(z) = x || // Complex. Both x and z are now in scope. ``` -This is not a simple construct, and could be quite confusing to newcomers +This is not a simple construct, and could be quite confusing to newcomers. That said, such a thing is not perfectly obvious to write today, and might be just as confusing to read: ```rust From b6b87d6a9f32de75f0a25c731404166881eda750 Mon Sep 17 00:00:00 2001 From: Jeremiah Senkpiel Date: Tue, 20 Jul 2021 15:51:58 -0700 Subject: [PATCH 24/25] let-else, disallow any expr ending with `}`. As per discussion in GitHub and Zulip, this seems like the most straightforward path until practical experimentation can be done. --- text/0000-let-else.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/text/0000-let-else.md b/text/0000-let-else.md index 785e0f01fc6..6fab556d830 100644 --- a/text/0000-let-else.md +++ b/text/0000-let-else.md @@ -209,15 +209,13 @@ let (each, binding) = match expr { ``` Most expressions may be put into the expression position with two restrictions: -1. May not include a block outside of parenthesis. - - Must be an [`ExpressionWithoutBlock`][expressions]. - - [`GroupedExpression`][grouped-expr]-s ending with a `}` are additionally not allowed and must be put in parenthesis. +1. May not end with a `}` (before macro expansion). (Such things must be put in parenthesis.) 2. May not be just a lazy boolean expression (`&&` or `||`). (Must not be a [`LazyBooleanExpression`][lazy-boolean-operators].) While allowing e.g. `if {} else {}` directly in the expression position is technically feasible this RFC proposes it be disallowed for programmer clarity so as to avoid `... else {} else {}` situations as discussed in the [drawbacks][] section. Boolean matches are not useful with let-else and so lazy boolean expressions are disallowed for reasons noted in [future-possibilities][]. -These types of expressions can still be used when combined in a less ambiguous manner with parenthesis, thus forming a [`GroupedExpression`][grouped-expr], +These types of expressions can still be used when combined in a less ambiguous manner with parenthesis, which is allowed under the two expression restrictions. Any refutable pattern that could be put into if-let's pattern position can be put into let-else's pattern position. @@ -278,7 +276,7 @@ because the compiler won't interpret it as `let PATTERN = (if y { a }) else { b This can be overcome by making a raw if-else in the expression position a compile error and instead requiring that parentheses are inserted to disambiguate: `let PATTERN = (if { a } else { b }) else { c };`. -Rust already provides us with such a restriction, and so the expression can be restricted to be a [`ExpressionWithoutBlock`][expressions]. +This restriction can be made by checking if the expression ends in `}` after parsing but _before_ macro expansion. # Rationale and alternatives [rationale-and-alternatives]: #rationale-and-alternatives @@ -304,7 +302,7 @@ let true = a && b else { }; ``` -The expression can be any [`ExpressionWithoutBlock`][expressions], in order to prevent `else {} else {}` confusion, as noted in [drawbacks][#drawbacks]. +The expression must not end with a `}`, in order to prevent `else {} else {}` (and similar) confusion, as noted in [drawbacks][#drawbacks]. The `else` must be followed by a block, as in `if {} else {}`. This else block must be diverging as the outer context cannot be guaranteed to continue soundly without assignment, and no alternate assignment syntax is provided. @@ -643,7 +641,6 @@ because it is confusing to read syntactically, and it is functionally similar to [`const`]: https://doc.rust-lang.org/reference/items/constant-items.html [`static`]: https://doc.rust-lang.org/reference/items/static-items.html [expressions]: https://doc.rust-lang.org/reference/expressions.html#expressions -[grouped-expr]: https://doc.rust-lang.org/reference/expressions/grouped-expr.html [guard-crate]: https://crates.io/crates/guard [guard-repo]: https://github.com/durka/guard [if-let]: https://rust-lang.github.io/rfcs/0160-if-let.html From 952745bb935ae5d65b02cb0009831019d5e406f8 Mon Sep 17 00:00:00 2001 From: Jeremiah Senkpiel Date: Tue, 20 Jul 2021 15:59:19 -0700 Subject: [PATCH 25/25] let-else, mention macro expansions Forgot this in the last commit. Invisible groupings from macros should be allowed but should be shown to humans in visible expansion tools. --- text/0000-let-else.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/text/0000-let-else.md b/text/0000-let-else.md index 6fab556d830..28f78e0e06b 100644 --- a/text/0000-let-else.md +++ b/text/0000-let-else.md @@ -217,6 +217,8 @@ disallowed for programmer clarity so as to avoid `... else {} else {}` situation Boolean matches are not useful with let-else and so lazy boolean expressions are disallowed for reasons noted in [future-possibilities][]. These types of expressions can still be used when combined in a less ambiguous manner with parenthesis, which is allowed under the two expression restrictions. +Invisible groupings from macros expansions are also allowed, however macro expansion representations to humans should include parenthesis +around the expression output in this position if it ends in a `}` where possible (or otherwise show the invisible grouping). Any refutable pattern that could be put into if-let's pattern position can be put into let-else's pattern position.