Skip to content

Commit 2b92ef5

Browse files
committed
let-else first draft
1 parent 43185c8 commit 2b92ef5

File tree

1 file changed

+329
-0
lines changed

1 file changed

+329
-0
lines changed

text/0000-let-else.md

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
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

Comments
 (0)