|
| 1 | +- Feature Name: `let-else` |
| 2 | +- Start Date: 2021-05-31 |
| 3 | +- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000) |
| 4 | +- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) |
| 5 | + |
| 6 | +# Summary |
| 7 | +[summary]: #summary |
| 8 | + |
| 9 | +Introduce a new `let PATTERN = EXPRESSION else { /* DIVERGING BLOCK */ };` construct (informally called a |
| 10 | +**let-else statement**), the counterpart of if-let expressions. |
| 11 | + |
| 12 | +If the pattern match from the assigned expression succeeds, its bindings are introduced *into the |
| 13 | +surrounding scope*. If it does not succeed, it must diverge (e.g. return or break). |
| 14 | +let-else statements are refutable `let` statements. |
| 15 | + |
| 16 | +This RFC is a modernization of a [2015 RFC (pull request 1303)][old-rfc] for an almost identical feature. |
| 17 | + |
| 18 | +# Motivation |
| 19 | +[motivation]: #motivation |
| 20 | + |
| 21 | +`let else` simplifies some very common error-handling patterns. |
| 22 | +It is the natural counterpart to `if let`, just as `else` is to regular `if`. |
| 23 | + |
| 24 | +[if-let expressions][if-let] offer a succinct syntax for pattern matching single patterns. |
| 25 | +This is particularly useful for unwrapping types like `Option`, particularly those with a clear "success" varient |
| 26 | +for the given context but no specific "failure" varient. |
| 27 | +However, an if-let expression can only create bindings within its body, which can force |
| 28 | +rightward drift, introduce excessive nesting, and separate conditionals from error paths. |
| 29 | + |
| 30 | +let-else statements move the "failure" case into the body block, while allowing |
| 31 | +the "success" case to continue in the surrounding context without additional nesting. |
| 32 | + |
| 33 | +let-else statements are also more succinct and natural than emulating the equivalent pattern with `match` or if-let, |
| 34 | +which require intermediary bindings (usually of the same name). |
| 35 | + |
| 36 | +## Examples |
| 37 | + |
| 38 | +The following three code examples are possible options with current Rust code. |
| 39 | + |
| 40 | +```rust |
| 41 | +if let Some(a) = x { |
| 42 | + if let Some(b) = y { |
| 43 | + if let Some(c) = z { |
| 44 | + // ... |
| 45 | + do_something_with(a, b, c); |
| 46 | + // ... |
| 47 | + } else { |
| 48 | + return Err("bad z"); |
| 49 | + } |
| 50 | + } else { |
| 51 | + return Err("bad y"); |
| 52 | + } |
| 53 | +} else { |
| 54 | + return Err("bad x"); |
| 55 | +} |
| 56 | +``` |
| 57 | + |
| 58 | +```rust |
| 59 | +let a = match x { |
| 60 | + Some(a) => a, |
| 61 | + _ => return Err("bad x"), |
| 62 | +} |
| 63 | +let b = match y { |
| 64 | + Some(b) => b, |
| 65 | + _ => return Err("bad y"), |
| 66 | +} |
| 67 | +let c = match z { |
| 68 | + Some(c) => c, |
| 69 | + _ => return Err("bad z"), |
| 70 | +} |
| 71 | +// ... |
| 72 | +do_something_with(a, b, c); |
| 73 | +// ... |
| 74 | +``` |
| 75 | + |
| 76 | +```rust |
| 77 | +let a = if let Some(a) { a } else { |
| 78 | + return Err("bad x"), |
| 79 | +}; |
| 80 | +let b = if let Some(b) { b } else { |
| 81 | + return Err("bad y"), |
| 82 | +}; |
| 83 | +let c = if let Some(c) { c } else { |
| 84 | + return Err("bad z"), |
| 85 | +}; |
| 86 | +// ... |
| 87 | +do_something_with(a, b, c); |
| 88 | +// ... |
| 89 | +``` |
| 90 | + |
| 91 | +All three of the above examples would be able to be written as: |
| 92 | + |
| 93 | +```rust |
| 94 | +let Some(a) = x else { |
| 95 | + return Err("bad x"); |
| 96 | +} |
| 97 | +let Some(b) = y else { |
| 98 | + return Err("bad y"); |
| 99 | +} |
| 100 | +let Some(c) = z else { |
| 101 | + return Err("bad z"); |
| 102 | +} |
| 103 | +// ... |
| 104 | +do_something_with(a, b, c); |
| 105 | +// ... |
| 106 | +``` |
| 107 | + |
| 108 | +which succinctly avoids bindings of the same name, rightward shift, etc. |
| 109 | + |
| 110 | +## Versus `match` |
| 111 | + |
| 112 | +It is possible to use `match` statements to emulate this today, but at a |
| 113 | +significant cost in length and readability. For example, this real-world code |
| 114 | +from Servo: |
| 115 | + |
| 116 | +```rust |
| 117 | +let subpage_layer_info = match layer_properties.subpage_layer_info { |
| 118 | + Some(ref subpage_layer_info) => *subpage_layer_info, |
| 119 | + None => return, |
| 120 | +}; |
| 121 | +``` |
| 122 | + |
| 123 | +is equivalent to this much simpler let-else statement: |
| 124 | + |
| 125 | +```rust |
| 126 | +let Some(ref subpage_layer_info) = layer_properties.subpage_layer_info else { |
| 127 | + return |
| 128 | +} |
| 129 | +``` |
| 130 | + |
| 131 | +# Guide-level explanation |
| 132 | +[guide-level-explanation]: #guide-level-explanation |
| 133 | + |
| 134 | +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, |
| 135 | +and "return early", by constructing an error `Result` or an empty `Option`, and returning it before the "happy path" code. |
| 136 | + |
| 137 | +This pattern serves no practical purpose to a computer, but it is helpful for humans interacting with the code. |
| 138 | +Returning early helps improve code clarity in two ways: |
| 139 | +- Ensuring the returned result in near the conditional, visually, as the following logic may be lengthy. |
| 140 | +- Reduces rightward shift, as the error return is now in the block, rather than the following logic. |
| 141 | + |
| 142 | +This RFC proposes _(Rust provides)_ an extension to `let` assignment statements to help with this pattern, an `else { }` which can follow a pattern match |
| 143 | +as a `let` assigning statement: |
| 144 | + |
| 145 | +```rust |
| 146 | +let Some(a) = an_option else { |
| 147 | + // Called if `an_option` is not `Option::Some(T)`. |
| 148 | + // This block must diverge (stop executing the existing context to the parent block or function). |
| 149 | + return; |
| 150 | +}; |
| 151 | + |
| 152 | +// `a` is now in scope and is the type which the `Option` contained. |
| 153 | +``` |
| 154 | + |
| 155 | +This is a counterpart to `if let` expressions, and the pattern matching works identically, except that the value from the pattern match |
| 156 | +is assigned to the surrounding scope rather than the block's scope. |
| 157 | + |
| 158 | +# Reference-level explanation |
| 159 | +[reference-level-explanation]: #reference-level-explanation |
| 160 | + |
| 161 | +let-else is syntactical sugar for either `if let { assignment } else {}` or `match`, where the non-matched case diverges. |
| 162 | + |
| 163 | +Any expression may be put into the expression position except an `if {} else {}` as explain below in [drawbacks][]. |
| 164 | +While `if {} else {}` is technically feasible this RFC proposes it be disallowed for programmer clarity to avoid an `... else {} else {}` situation. |
| 165 | + |
| 166 | +Any pattern that could be put into if-let's pattern position can be put into let-else's pattern position. |
| 167 | + |
| 168 | +The `else` block must diverge. This could be a keyword which diverges (returns `!`), or a panic. |
| 169 | +Allowed keywords: |
| 170 | +- `return` |
| 171 | +- `break` |
| 172 | +- `continue` |
| 173 | + |
| 174 | +If the pattern does not match, the expression is not consumed, and so any existing variables from the surrounding scope are |
| 175 | +accessible as they would normally be. |
| 176 | + |
| 177 | +# Drawbacks |
| 178 | +[drawbacks]: #drawbacks |
| 179 | + |
| 180 | +## The diverging block |
| 181 | + |
| 182 | +"Must diverge" is an unusual requirement, which doesn't exist elsewhere in the language as of the time of writing, |
| 183 | +and might be difficult to explain or lead to confusing errors for programmers new to this feature. |
| 184 | + |
| 185 | +## `let PATTERN = if {} else {} else {};` |
| 186 | + |
| 187 | +One unfortunate combination of this feature with regular if-else expressions is the possibility of `let PATTERN = if { a } else { b } else { c };`. |
| 188 | +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 |
| 189 | +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: ...`) |
| 190 | +because the compiler won't interpret it as `let PATTERN = (if y { a }) else { b };` since `()` is not an enum. |
| 191 | + |
| 192 | +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: |
| 193 | +`let PATTERN = (if { a } else { b }) else { c };`. |
| 194 | + |
| 195 | +# Rationale and alternatives |
| 196 | +[rationale-and-alternatives]: #rationale-and-alternatives |
| 197 | + |
| 198 | +let-else attempts to be as consistent as possible to similar existing syntax. |
| 199 | + |
| 200 | +Fundimentally it is treated as a `let` statement, necessitating an assignment and the trailing semicolon. |
| 201 | + |
| 202 | +Pattern matching works identically to if-let, no new "negation" pattern matching rules are introduced. |
| 203 | + |
| 204 | +The `else` must be followed by a block, as in `if {} else {}`. |
| 205 | + |
| 206 | +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. |
| 207 | + |
| 208 | +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 (`?`), |
| 209 | +such functions do not exist automatically on custom enum types and require non-obvious and non-trivial implementation, and may not be map-able |
| 210 | +to `Option`/`Result`-style functions at all (especially for enums where the "success" varient is contextual and there are many varients). |
| 211 | + |
| 212 | +## Alternatives |
| 213 | + |
| 214 | +### `let PATTERN = EXPR else return EXPR;` |
| 215 | + |
| 216 | +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 |
| 217 | +but instead an expression which starts with a diverging keyword, such as `return` or `break`. |
| 218 | + |
| 219 | +Example: |
| 220 | +``` |
| 221 | +let Some(foo) = some_option else return None; |
| 222 | +``` |
| 223 | + |
| 224 | +This RFC avoids this because it is overall less consistent with `else` from if-else, which require blocks. |
| 225 | + |
| 226 | +This was originally suggested in the old RFC, comment at https://github.com/rust-lang/rfcs/pull/1303#issuecomment-188526691 |
| 227 | + |
| 228 | +### `else`-block fall-back assignment |
| 229 | + |
| 230 | +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. |
| 231 | + |
| 232 | +This RFC avoids this proposal, because there is no clear syntax to use for it which would be consistent with other existing features. |
| 233 | +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. |
| 234 | +This RFC proposes that most fallback cases are sufficiently or better covered by using `match`. |
| 235 | + |
| 236 | +An example, using a proposal to have the binding be visible and assignable from the `else`-block. |
| 237 | +Note that this is incompatible with this RFC and could probably not be added as an extension from this RFC. |
| 238 | + |
| 239 | +```rust |
| 240 | +enum AnEnum { |
| 241 | + Varient1(u32), |
| 242 | + Varient2(String), |
| 243 | +} |
| 244 | + |
| 245 | +let AnEnum::Varient1(a) = x else { |
| 246 | + a = 42; |
| 247 | +}; |
| 248 | +``` |
| 249 | + |
| 250 | +Another potential alternative for fall-back which could be added with an additional keyword as a future extension: |
| 251 | + |
| 252 | +```rust |
| 253 | +enum AnEnum { |
| 254 | + Varient1(u32), |
| 255 | + Varient2(String), |
| 256 | +} |
| 257 | + |
| 258 | +let AnEnum::Varient1(a) = x else assign a { |
| 259 | + a = 42; |
| 260 | +}; |
| 261 | +``` |
| 262 | + |
| 263 | +### `if !let PAT = EXPR { BODY }` |
| 264 | + |
| 265 | +The [old RFC][old-rfc] originally proposed this general feature via some kind of pattern negation as `if !let PAT = EXPR { BODY }`. |
| 266 | + |
| 267 | +This RFC avoids adding any kind of new or special pattern matching rules. The pattern matching works as it does for if-let. |
| 268 | +The general consensus in the old RFC was also that the negation syntax is much less clear than `if PATTERN = EXPR else { /* diverge */ };`, |
| 269 | +and partway through that RFC's lifecycle it was updated to be similar to this RFC's proposed let-else syntax. |
| 270 | + |
| 271 | +### Complete Alternative |
| 272 | + |
| 273 | +- 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. |
| 274 | + |
| 275 | +# Prior art |
| 276 | +[prior-art]: #prior-art |
| 277 | + |
| 278 | +This RFC is a modernization of a [2015 RFC (pull request 1303)][old-rfc]. |
| 279 | + |
| 280 | +A lot of this RFC's proposals come from that RFC and its ensuing discussions. |
| 281 | + |
| 282 | +The Swift programming language, which inspired Rust's if-let expression, also |
| 283 | +includes a [guard-let-else][swift] statement which is equivalent to this |
| 284 | +proposal except for the choice of keywords. |
| 285 | + |
| 286 | +The `match` alternative in particular is fairly prevalent in rust code on projects which have many possible error conditions. |
| 287 | + |
| 288 | +The Try operator allows for an `ok_or` alternative to be used where the types are only `Option` and `Result`, |
| 289 | +which is considered to be idomatic rust. |
| 290 | + |
| 291 | +// TODO link to examples, provide internal stistics, gather statistics from the rust compiler itself, etc. |
| 292 | + |
| 293 | +# Unresolved questions |
| 294 | +[unresolved-questions]: #unresolved-questions |
| 295 | + |
| 296 | +None known at time of writing due to extensive pre-discussion in Zulip: |
| 297 | +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 |
| 298 | + |
| 299 | +# Future possibilities |
| 300 | +[future-possibilities]: #future-possibilities |
| 301 | + |
| 302 | +## Fall-back assignment |
| 303 | + |
| 304 | +This RFC does not suggest that we do any of these, but notes that they would be future possibilities. |
| 305 | + |
| 306 | +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: |
| 307 | + |
| 308 | +```rust |
| 309 | +enum AnEnum { |
| 310 | + Varient1(u32), |
| 311 | + Varient2(String), |
| 312 | +} |
| 313 | + |
| 314 | +let AnEnum::Varient1(a) = x else assign a { |
| 315 | + a = 42; |
| 316 | +}; |
| 317 | +``` |
| 318 | + |
| 319 | +Another potential form of the fall-back extension: |
| 320 | + |
| 321 | +```rust |
| 322 | +let Ok(a) = x else match { |
| 323 | + Err(e) => return Err(e.into()), |
| 324 | +} |
| 325 | +``` |
| 326 | + |
| 327 | +[old-rfc]: https://github.com/rust-lang/rfcs/pull/1303 |
| 328 | +[if-let]: https://github.com/rust-lang/rfcs/blob/master/text/0160-if-let.md |
| 329 | +[swift]: https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/ControlFlow.html#//apple_ref/doc/uid/TP40014097-CH9-ID525 |
0 commit comments