|
| 1 | +- Feature Name: N/A |
| 2 | +- Start Date: 2017-12-16 |
| 3 | +- RFC PR: [rust-lang/rfcs#2250](https://github.com/rust-lang/rfcs/pull/2250) |
| 4 | +- Rust Issue: [rust-lang/rust#34511](https://github.com/rust-lang/rust/issues/34511) |
| 5 | + |
| 6 | +# Summary |
| 7 | +[summary]: #summary |
| 8 | + |
| 9 | +Finalize syntax of `impl Trait` and `dyn Trait` with multiple bounds before |
| 10 | +stabilization of these features. |
| 11 | + |
| 12 | +# Motivation |
| 13 | +[motivation]: #motivation |
| 14 | + |
| 15 | +Current priority of `+` in `impl Trait1 + Trait2` / `dyn Trait1 + Trait2` brings |
| 16 | +inconsistency in the type grammar. |
| 17 | +This RFC outlines possible syntactic |
| 18 | +alternatives and suggests one of them for stabilization. |
| 19 | + |
| 20 | +# Guide-level explanation |
| 21 | +[guide-level-explanation]: #guide-level-explanation |
| 22 | + |
| 23 | +"Alternative 2" (see reference-level explanation) is selected for stabilization. |
| 24 | + |
| 25 | +`impl Trait1 + Trait2` / `dyn Trait1 + Trait2` now require parentheses in all |
| 26 | +contexts where they are used inside of unary operators `&(impl Trait1 + Trait2)` |
| 27 | +/ `&(dyn Trait1 + Trait2)`, similarly to trait object types without |
| 28 | +prefix, e.g. `&(Trait1 + Trait2)`. |
| 29 | + |
| 30 | +Additionally, parentheses are required in all cases where `+` in `impl` or `dyn` |
| 31 | +is ambiguous. |
| 32 | +For example, `Fn() -> impl A + B` can be interpreted as both |
| 33 | +`(Fn() -> impl A) + B` (low priority plus) or `Fn() -> (impl A + B)` (high |
| 34 | +priority plus), so we are refusing to disambiguate and require explicit |
| 35 | +parentheses. |
| 36 | + |
| 37 | +# Reference-level explanation |
| 38 | +[reference-level-explanation]: #reference-level-explanation |
| 39 | + |
| 40 | +## Current situation |
| 41 | + |
| 42 | +In the current implementation when we see `impl` or `dyn` we start parsing |
| 43 | +following bounds separated by `+`s greedily regardless of context, so `+` |
| 44 | +effectively gets the strongest priority. |
| 45 | + |
| 46 | +So, for example: |
| 47 | +- `&dyn A + B` is parsed as `&(dyn A + B)` |
| 48 | +- `Fn() -> impl A + B` is parsed as `Fn() -> (impl A + B)` |
| 49 | +- `x as &dyn A + y` is parsed as `x as &(dyn A + y)`. |
| 50 | + |
| 51 | +Compare this with parsing of trait object types without prefixes |
| 52 | +([RFC 438](https://github.com/rust-lang/rfcs/pull/438)): |
| 53 | +- `&A + B` is parsed as `(&A) + B` and is an error |
| 54 | +- `Fn() -> A + B` is parsed as `(Fn() -> A) + B` |
| 55 | +- `x as &A + y` is parsed as `(x as &A) + y` |
| 56 | + |
| 57 | +Also compare with unary operators in bounds themselves: |
| 58 | +- `for<'a> A<'a> + B` is parsed as `(for<'a> A<'a>) + B`, |
| 59 | +not `for<'a> (A<'a> + B)` |
| 60 | +- `?A + B` is parsed as `(?A) + B`, not `?(A + B)` |
| 61 | + |
| 62 | +In general, binary operations like `+` have lower priority than unary operations |
| 63 | +in all contexts - expressions, patterns, types. So the priorities as implemented |
| 64 | +bring inconsistency and may break intuition. |
| 65 | + |
| 66 | +## Alternative 1: high priority `+` (status quo) |
| 67 | + |
| 68 | +Pros: |
| 69 | +- The greedy parsing with high priority of `+` after `impl` / `dyn` |
| 70 | +has one benefit - it requires the least amout of parentheses from all the |
| 71 | +alternatives. |
| 72 | +Parentheses are needed only when the greedy behaviour needs to be prevented, |
| 73 | +e.g. `Fn() -> &(dyn Write) + Send`, this doesn't happen often. |
| 74 | + |
| 75 | +Cons: |
| 76 | +- Inconsistent and possibly surprising operator priorities. |
| 77 | +- `impl` / `dyn` is a somewhat weird syntactic construction, it's not an usual |
| 78 | +unary operator, its a prefix describing how to interpret the following tokens. |
| 79 | +In particular, if the `impl A + B` needs to be parenthesized for some reason, |
| 80 | +it needs to be done like this `(impl A + B)`, and not `impl (A + B)`. The second |
| 81 | +variant is a parsing error, but some people find it surprising and expect it to |
| 82 | +work, as if `impl` were an unary operator. |
| 83 | + |
| 84 | +## Alternative 2: low priority `+` |
| 85 | + |
| 86 | +Basically, `impl A + B` is parsed using same rules as `A + B`. |
| 87 | + |
| 88 | +If `impl A + B` is located inside a higher priority operator like `&` it has |
| 89 | +to be parenthesized. |
| 90 | +If it is located at intersection of type and expressions |
| 91 | +grammars like `expr1 as Type + expr2`, it has to be parenthesized as well. |
| 92 | + |
| 93 | +`&dyn A + B` / `Fn() -> impl A + B` / `x as &dyn A + y` has to be rewritten as |
| 94 | +`&(dyn A + B)` / `Fn() -> (impl A + B)` / `x as &(dyn A + y)` respectively. |
| 95 | + |
| 96 | +One location must be mentioned specially, the location in a function return |
| 97 | +type: |
| 98 | +```rust |
| 99 | +fn f() -> impl A + B |
| 100 | +{ |
| 101 | + // Do things |
| 102 | +} |
| 103 | +``` |
| 104 | +This is probably the most common location for `impl Trait` types. |
| 105 | +In theory, it doesn't require parentheses in any way - it's not inside of an |
| 106 | +unary operator and it doesn't cross expression boundaries. |
| 107 | +However, it creates a bit of percieved inconsistency with function-like traits |
| 108 | +and function pointers that do require parentheses for `impl Trait` in return |
| 109 | +types (`Fn() -> (impl A + B)` / `fn() -> (impl A + B)`) because they, in their |
| 110 | +turn, can appear inside of unary operators and casts. |
| 111 | +So, if avoiding this is considered more important than ergonomics, then |
| 112 | +we can require parentheses in function definitions as well. |
| 113 | +```rust |
| 114 | +fn f() -> (impl A + B) |
| 115 | +{ |
| 116 | + // Do things |
| 117 | +} |
| 118 | +``` |
| 119 | + |
| 120 | +Pros: |
| 121 | +- Consistent priorities of binary and unary operators. |
| 122 | +- Parentheses are required relatively rarely (unless we require them in |
| 123 | +function definitions as well). |
| 124 | + |
| 125 | +Cons: |
| 126 | +- More parentheses than in the "Alternative 1". |
| 127 | +- `impl` / `dyn` is still a somewhat weird prefix construction and `dyn (A + B)` |
| 128 | +is not a valid syntax. |
| 129 | + |
| 130 | +## Alternative 3: Unary operator |
| 131 | + |
| 132 | +`impl` and `dyn` can become usual unary operators in type grammar like `&` or |
| 133 | +`*const`. |
| 134 | +Their application to any other types except for (possibly parenthesized) paths |
| 135 | +(single `A`) or "legacy trait objects" (`A + B`) becomes an error, but this |
| 136 | +could be changed in the future if some other use is found. |
| 137 | + |
| 138 | +`&dyn A + B` / `Fn() -> impl A + B` / `x as &dyn A + y` has to be rewritten as |
| 139 | +`&dyn(A + B)` / `Fn() -> impl(A + B)` / `x as &dyn(A + y)` respectively. |
| 140 | + |
| 141 | +Function definitions with `impl A + B` in return type have to be rewritten too. |
| 142 | +```rust |
| 143 | +fn f() -> impl(A + B) |
| 144 | +{ |
| 145 | + // Do things |
| 146 | +} |
| 147 | +``` |
| 148 | + |
| 149 | +Pros: |
| 150 | +- Consistent priorities of binary and unary operators. |
| 151 | +- `impl` / `dyn` are usual unary operators, `dyn (A + B)` is a valid syntax. |
| 152 | + |
| 153 | +Cons: |
| 154 | +- The largest amount of parentheses, parentheses are always required. |
| 155 | +Parentheses are noise, there may be even less desire to use `dyn` in trait |
| 156 | +objects now, if something like `Box<Write + Send>` turns into |
| 157 | +`Box<dyn(Write + Send)>`. |
| 158 | + |
| 159 | +## Other alternatives |
| 160 | + |
| 161 | +Two separate grammars can be used depending on context |
| 162 | +(https://github.com/rust-lang/rfcs/pull/2250#issuecomment-352435687) - |
| 163 | +Alternative 1/2 in lists of arguments like `Box<dyn A + B>` or |
| 164 | +`Fn(impl A + B, impl A + B)`, and Alternative 3 otherwise (`&dyn (A + B)`). |
| 165 | + |
| 166 | +## Compatibility |
| 167 | + |
| 168 | +The alternatives are ordered by strictness from the most relaxed Alternative 1 |
| 169 | +to the strictest Alternative 3, but switching from more strict alternatives to |
| 170 | +less strict is not exactly backward-compatible. |
| 171 | + |
| 172 | +Switching from 2/3 to 1 can change meaning of legal code in rare cases. |
| 173 | +Switching from 3 to 2/1 requires keeping around the syntax with parentheses |
| 174 | +after `impl` / `dyn`. |
| 175 | + |
| 176 | +Alternative 2 can be backward-compatibly extended to "relaxed 3" in which |
| 177 | +parentheses like `dyn (A + B)` are permitted, but technically unnecessary. |
| 178 | +Such parens may keep people expecting `dyn (A + B)` to work happy, but |
| 179 | +complicate parsing by introducing more ambiguities to the grammar. |
| 180 | + |
| 181 | +While unary operators like `&` "obviously" have higher priority than `+`, |
| 182 | +cases like `Fn() -> impl A + B` are not so obvious. |
| 183 | +The Alternative 2 considers "low priority plus" to have lower priority than `Fn` |
| 184 | +, so `Fn() -> impl A + B` can be treated as `(Fn() -> impl A) + B`, however |
| 185 | +it may be more intuitive and consistent with `fn` items to make `+` have higher |
| 186 | +priority than `Fn` (but still lower priority than `&`). |
| 187 | +As an immediate solution we refuse to disambiguate this case and treat |
| 188 | +`Fn() -> impl A + B` as an error, so we can change the rules in the future and |
| 189 | +interpret `Fn() -> impl A + B` (and maybe even `Fn() -> A + B` after long |
| 190 | +deprecation period) as `Fn() -> (impl A + B)` (and `Fn() -> (A + B)`, |
| 191 | +respectively). |
| 192 | + |
| 193 | +## Experimental check |
| 194 | + |
| 195 | +An application of all the alternatives to rustc and libstd codebase can be found |
| 196 | +in [this branch](https://github.com/petrochenkov/rust/commits/impldyntest). |
| 197 | +The first commit is the baseline (Alternative 1) and the next commits show |
| 198 | +changes required to move to Alternatives 2 and 3. Alternative 2 requires fewer |
| 199 | +changes compared to Alternative 3. |
| 200 | + |
| 201 | +As the RFC author interprets it, the Alternative 3 turns out to be impractical |
| 202 | +due to common use of `Box`es and other contexts where the parens are technically |
| 203 | +unnecessary, but required by Alternative 3. |
| 204 | +The number of parens required by Alternative 2 is limited and they seem |
| 205 | +appropriate because they follow "normal" priorities for unary and binary |
| 206 | +operators. |
| 207 | + |
| 208 | +# Drawbacks |
| 209 | +[drawbacks]: #drawbacks |
| 210 | + |
| 211 | +See above. |
| 212 | + |
| 213 | +# Rationale and alternatives |
| 214 | +[alternatives]: #alternatives |
| 215 | + |
| 216 | +See above. |
| 217 | + |
| 218 | +# Unresolved questions |
| 219 | +[unresolved]: #unresolved-questions |
| 220 | + |
| 221 | +None. |
0 commit comments