Skip to content

Commit dfa27bb

Browse files
authored
Merge pull request #2250 from petrochenkov/impldyn
mini-RFC: Finalize syntax of `impl Trait` and `dyn Trait` with multiple bounds
2 parents d63632f + 8a88953 commit dfa27bb

File tree

1 file changed

+221
-0
lines changed

1 file changed

+221
-0
lines changed

text/2250-finalize-impl-dyn-syntax.md

+221
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
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

Comments
 (0)