|
| 1 | +- Feature Name: `if_let_guard` |
| 2 | +- Start Date: 2018-01-15 |
| 3 | +- RFC PR: [rust-lang/rfcs#2294](https://github.com/rust-lang/rfcs/pull/2294) |
| 4 | +- Rust Issue: [rust-lang/rust#51114](https://github.com/rust-lang/rust/issues/51114) |
| 5 | + |
| 6 | +# Summary |
| 7 | +[summary]: #summary |
| 8 | + |
| 9 | +Allow `if let` guards in `match` expressions. |
| 10 | + |
| 11 | +# Motivation |
| 12 | +[motivation]: #motivation |
| 13 | + |
| 14 | +This feature would greatly simplify some logic where we must match a pattern iff some value computed from the `match`-bound values has a certain form, where said value may be costly or impossible (due to affine semantics) to recompute in the match arm. |
| 15 | + |
| 16 | +For further motivation, see the example in the guide-level explanation. Absent this feature, we might rather write the following: |
| 17 | +```rust |
| 18 | +match ui.wait_event() { |
| 19 | + KeyPress(mod_, key, datum) => |
| 20 | + if let Some(action) = intercept(mod_, key) { act(action, datum) } |
| 21 | + else { accept!(KeyPress(mod_, key, datum)) /* can't re-use event verbatim if `datum` is non-`Copy` */ } |
| 22 | + ev => accept!(ev), |
| 23 | +} |
| 24 | +``` |
| 25 | + |
| 26 | +`accept` may in general be lengthy and inconvenient to move into another function, for example if it refers to many locals. |
| 27 | + |
| 28 | +Here is an (incomplete) example taken from a real codebase, to respond to ANSI CSI escape sequences: |
| 29 | + |
| 30 | +```rust |
| 31 | +#[inline] |
| 32 | +fn csi_dispatch(&mut self, parms: &[i64], ims: &[u8], ignore: bool, x: char) { |
| 33 | + match x { |
| 34 | + 'C' => if let &[n] = parms { self.screen.move_x( n as _) } |
| 35 | + else { log_debug!("Unknown CSI sequence: {:?}, {:?}, {:?}, {:?}", |
| 36 | + parms, ims, ignore, x) }, |
| 37 | + 'D' => if let &[n] = parms { self.screen.move_x(-n as _) } |
| 38 | + else { log_debug!("Unknown CSI sequence: {:?}, {:?}, {:?}, {:?}", |
| 39 | + parms, ims, ignore, x) }, |
| 40 | + 'J' => self.screen.erase(match parms { |
| 41 | + &[] | |
| 42 | + &[0] => Erasure::ScreenFromCursor, |
| 43 | + &[1] => Erasure::ScreenToCursor, |
| 44 | + &[2] => Erasure::Screen, |
| 45 | + _ => { log_debug!("Unknown CSI sequence: {:?}, {:?}, {:?}, {:?}", |
| 46 | + parms, ims, ignore, x); return }, |
| 47 | + }, false), |
| 48 | + 'K' => self.screen.erase(match parms { |
| 49 | + &[] | |
| 50 | + &[0] => Erasure::LineFromCursor, |
| 51 | + &[1] => Erasure::LineToCursor, |
| 52 | + &[2] => Erasure::Line, |
| 53 | + _ => { log_debug!("Unknown CSI sequence: {:?}, {:?}, {:?}, {:?}", |
| 54 | + parms, ims, ignore, x); return }, |
| 55 | + }, false), |
| 56 | + 'm' => match parms { |
| 57 | + &[] | |
| 58 | + &[0] => *self.screen.def_attr_mut() = Attr { fg_code: 0, fg_rgb: [0xFF; 3], |
| 59 | + bg_code: 0, bg_rgb: [0x00; 3], |
| 60 | + flags: AttrFlags::empty() }, |
| 61 | + &[n] => if let (3, Some(rgb)) = (n / 10, color_for_code(n % 10, 0xFF)) { |
| 62 | + self.screen.def_attr_mut().fg_rgb = rgb; |
| 63 | + } else { |
| 64 | + log_debug!("Unknown CSI sequence: {:?}, {:?}, {:?}, {:?}", |
| 65 | + parms, ims, ignore, x); |
| 66 | + }, |
| 67 | + _ => log_debug!("Unknown CSI sequence: {:?}, {:?}, {:?}, {:?}", |
| 68 | + parms, ims, ignore, x), |
| 69 | + }, |
| 70 | + _ => log_debug!("Unknown CSI sequence: {:?}, {:?}, {:?}, {:?}", |
| 71 | + parms, ims, ignore, x), |
| 72 | + } |
| 73 | +} |
| 74 | +``` |
| 75 | + |
| 76 | +These examples are both clearer with `if let` guards as follows. Particularly in the latter example, in the author's opinion, the control flow is easier to follow. |
| 77 | + |
| 78 | +# Guide-level explanation |
| 79 | +[guide-level-explanation]: #guide-level-explanation |
| 80 | + |
| 81 | +*(Adapted from Rust book)* |
| 82 | + |
| 83 | +A *match guard* is an `if let` condition specified after the pattern in a `match` arm that also must match if the pattern matches in order for that arm to be chosen. Match guards are useful for expressing more complex ideas than a pattern alone allows. |
| 84 | + |
| 85 | +The condition can use variables created in the pattern, and the match arm can use any variables bound in the `if let` pattern (as well as any bound in the `match` pattern, unless the `if let` expression moves out of them). |
| 86 | + |
| 87 | +Let us consider an example which accepts a user-interface event (e.g. key press, pointer motion) and follows 1 of 2 paths: either we intercept it and take some action or deal with it normally (whatever that might mean here): |
| 88 | +```rust |
| 89 | +match ui.wait_event() { |
| 90 | + KeyPress(mod_, key, datum) if let Some(action) = intercept(mod_, key) => act(action, datum), |
| 91 | + ev => accept!(ev), |
| 92 | +} |
| 93 | +``` |
| 94 | + |
| 95 | +Here is another example, to respond to ANSI CSI escape sequences: |
| 96 | + |
| 97 | +```rust |
| 98 | +#[inline] |
| 99 | +fn csi_dispatch(&mut self, parms: &[i64], ims: &[u8], ignore: bool, x: char) { |
| 100 | + match x { |
| 101 | + 'C' if let &[n] = parms => self.screen.move_x( n as _), |
| 102 | + 'D' if let &[n] = parms => self.screen.move_x(-n as _), |
| 103 | + _ if let Some(e) = erasure(x, parms) => self.screen.erase(e, false), |
| 104 | + 'm' => match parms { |
| 105 | + &[] | |
| 106 | + &[0] => *self.screen.def_attr_mut() = Attr { fg_code: 0, fg_rgb: [0xFF; 3], |
| 107 | + bg_code: 0, bg_rgb: [0x00; 3], |
| 108 | + flags: AttrFlags::empty() }, |
| 109 | + &[n] if let (3, Some(rgb)) = (n / 10, color_for_code(n % 10, 0xFF)) => |
| 110 | + self.screen.def_attr_mut().fg_rgb = rgb, |
| 111 | + _ => log_debug!("Unknown CSI sequence: {:?}, {:?}, {:?}, {:?}", |
| 112 | + parms, ims, ignore, x), |
| 113 | + }, |
| 114 | + _ => log_debug!("Unknown CSI sequence: {:?}, {:?}, {:?}, {:?}", |
| 115 | + parms, ims, ignore, x), |
| 116 | + } |
| 117 | +} |
| 118 | + |
| 119 | +#[inline] |
| 120 | +fn erasure(x: char, parms: &[i64]) -> Option<Erasure> { |
| 121 | + match x { |
| 122 | + 'J' => match parms { |
| 123 | + &[] | |
| 124 | + &[0] => Some(Erasure::ScreenFromCursor), |
| 125 | + &[1] => Some(Erasure::ScreenToCursor), |
| 126 | + &[2] => Some(Erasure::Screen), |
| 127 | + _ => None, |
| 128 | + }, |
| 129 | + 'K' => match parms { |
| 130 | + &[] | |
| 131 | + &[0] => Some(Erasure::LineFromCursor), |
| 132 | + &[1] => Some(Erasure::LineToCursor), |
| 133 | + &[2] => Some(Erasure::Line), |
| 134 | + _ => None, |
| 135 | + }, |
| 136 | + _ => None, |
| 137 | + } |
| 138 | +} |
| 139 | +``` |
| 140 | + |
| 141 | + |
| 142 | +# Reference-level explanation |
| 143 | +[reference-level-explanation]: #reference-level-explanation |
| 144 | + |
| 145 | +This proposal would introduce syntax for a match arm: `pat if let guard_pat = guard_expr => body_expr` with semantics so the arm is chosen iff the argument of `match` matches `pat` and `guard_expr` matches `guard_pat`. The variables of `pat` are bound in `guard_expr`, and the variables of `pat` and `guard_pat` are bound in `body_expr`. The syntax is otherwise the same as for `if` guards. (Indeed, `if` guards become effectively syntactic sugar for `if let` guards.) |
| 146 | + |
| 147 | +An arm may not have both an `if` and an `if let` guard. |
| 148 | + |
| 149 | +# Drawbacks |
| 150 | +[drawbacks]: #drawbacks |
| 151 | + |
| 152 | +* It further complicates the grammar. |
| 153 | +* It is ultimately syntactic sugar, but the transformation to present Rust is potentially non-obvious. |
| 154 | + |
| 155 | +# Rationale and alternatives |
| 156 | +[alternatives]: #alternatives |
| 157 | + |
| 158 | +* The chief alternatives are to rewrite the guard as an `if` guard and a bind in the match arm, or in some cases into the argument of `match`; or to write the `if let` in the match arm and copy the rest of the `match` into the `else` branch — what can be done with this syntax can already be done in Rust (to the author's knowledge); this proposal is purely ergonomic, but in the author's opinion, the ergonomic win is significant. |
| 159 | +* The proposed syntax feels natural by analogy to the `if` guard syntax we already have, as between `if` and `if let` expressions. No alternative syntaxes were considered. |
| 160 | + |
| 161 | +# Unresolved questions |
| 162 | +[unresolved]: #unresolved-questions |
| 163 | + |
| 164 | +Questions in scope of this proposal: none yet known |
| 165 | + |
| 166 | +Questions out of scope: |
| 167 | + |
| 168 | +* Should we allow multiple guards? This proposal allows only a single `if let` guard. One can combine `if` guards with `&&` — [an RFC](https://github.com/rust-lang/rfcs/issues/929) to allow `&&` in `if let` already is, so we may want to follow that in future for `if let` guards also. |
| 169 | +* What happens if `guard_expr` moves out of `pat` but fails to match? This is already a question for `if` guards and (to the author's knowledge) not formally specified anywhere — this proposal (implicitly) copies that behavior. |
0 commit comments