-
Notifications
You must be signed in to change notification settings - Fork 12.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
syntax: Lower priority of +
in impl Trait
/dyn Trait
#45294
Conversation
While I understand the idea of trying to be as flexible as possible, I do wonder if it breaks the principle of least surprise to accept non-parenthesized Put another way: will users be more likely to write the correct thing for case of |
I'm not sure yet what I think about (On the other hand, since |
I had a similar reaction. (I also wonder if this is something we might consider tweaking in a Rust 2.0 Epoch -- i.e., we could encourage Rust 1.x users to write |
There's certainly a trade-off here between "least surprise" and "least annoyance". |
Why? The
This is certainly a viable alternative, maybe even more intuitive, even if it requires writing strictly more parens. Unnecessary parentheses for all sums are still pretty bad though. If I weren't followed the language closely and suddenly discovered that I need to change my |
Oh, also In #45175 it is disambiguated in favor of a function-like trait |
If |
Rather than changing the precedence, personally, if we decided to change this I'd just as soon make it non-associative, requiring you to write parentheses for either meaning. Because I think |
@joshtriplett x as &Trait + y
x as &dyn Trait + y This is currently parsed as type A = &A + B; // ERROR
type B = &dyn A + B; // OK Even if it's technically parseable and explainable, the inconsistency and the only precedent of binary "operator" having higher precedence than unary ones still annoy me. |
Parsing |
Maybe I should write a mini-RFC describing possible alternatives, to get some wider feedback? |
@petrochenkov sorry, I've been meaning to reply to this PR for a while now. I'd be in favor of a mini-RFC, or at least a good write-up. What I really want is an official rust grammar to express these changes in terms of. I'm sad we don't have that yet, though I think there are some unofficial ones that have progress quite far. Let me at least clarify how I expected it to work. My expectation was that we had a setup like this:
This is kinda' what we have now, or at least how I think of it, and it means that I can do (e.g.) However, I realize that this doesn't account for bounds that are not types, like |
OK, I see. You mean that right now |
I'll write an RFC. |
@petrochenkov are you still planning to write something up for this? |
@cramertj |
RFC is submitted |
@petrochenkov I'm trying to get |
@cramertj |
2ef5fe8
to
732a623
Compare
type A = fn() -> A + B; | ||
//~^ ERROR expected a path on the left-hand side of `+`, not `fn() -> A` | ||
|
||
type A = Fn() -> impl A + B; // OK, interpreted as `(Fn() -> impl A) + B` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we get a separate test showing that this errors? (Hmm, well, I suppose such a test already exists, right @cramertj ?)
//~^ ERROR expected a path on the left-hand side of `+`, not `fn() -> A` | ||
|
||
type A = Fn() -> impl A + B; // OK, interpreted as `(Fn() -> impl A) + B` | ||
type A = Fn() -> dyn A + B; // OK, interpreted as `(Fn() -> dyn A) + B` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But this case probably doesn't error -- I sort of think it should.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose we might be able to target it by linting. I did think though there was some desire to "put off" the question of just how Fn
and friends works when combined with dyn
and impl
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1-- I think users should have to manually specify parentheses here, at least for the time-being.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'll be a bit of a pain to do, I guess, since saying dyn A + B
and A + B
have same priority means that this case just kind of "falls out" this way.
But I still feel like we should keep future flexibility here.
@petrochenkov what do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clippy lint?
To put this into perspective - few people remember priorities of operators in expressions beyond *
vs +
and &&
vs ||
, but we still don't require parentheses everywhere.
Here if you get the priorities of Fn
and +
wrong, you won't even get runtime bugs, like with expressions, you'll get a type checking error about unsatisfied bounds, giving you opportunity to learn what priority is right.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More directly, I think the point was that we know we want this to work:
fn foo() -> impl A + B { }
and that therefore one might very well expect this to work:
where F: Fn() -> impl A + B
Now, historically, as @petrochenkov correctly points out, we opted not to make the where clause case "work" (that is, where F: Fn() -> A + B
parses two bounds on F
) because returning a value of type A + B
was almost certainly not what you wanted. However, introducing keywords like dyn
and impl
gives us a chance to revisit this decision backwards compatibly, and it is not clear that -- in this new context -- this is the correct behavior.
As @cramertj noted, I don't think that anyone feels the correct interpretation of where F: Fn() -> impl A + B
would be that B
is a bound on F
. (It's just a bit too surprising.) And moreover it seems clear that dyn
ought to behave the same as impl
. However, there were also those -- notably @joshtriplett -- who felt that there is no correct interpretation, and we should just avoid guessing. (I think that @joshtriplett would also be happy to have required parentheses in other cases where we do not today, e.g. amongst confusing operators.)
Given this, it does seem like we ought to try to disallow -> impl A + B
and -> dyn A + B
for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me try to clarify what I think we want. We already have two grammatical precedence levels for types. I'll them T0
and T1
. T0
includes all types, including the "n-ary" operators like dyn A + B
and A + B
. T1
excludes those n-ary operators, and is used primarily for &
types (which take a T1
argument).
Currently, in the parser, if we are parsing a T1
type and we see a +
we stop, leaving it to be consumed from some enclosing context.
I think that we want to have a rule such that T1 = dyn Identifier
parses, but if we see a +
after that, we error out (in the parser). This means that &dyn A + B
is a parse error, as is where F: Fn() -> dyn A + B
, and (I imagine) dyn Fn() -> dyn A + B
.
I think of this as being analogous to non-associativity: there are basically multiple interpretations of the +
operator. It can be adding bounds to a type parameter in a where-clause list; it can be adding types in a (old-style, A+B
) sum-type; and it can be adding bounds to a dyn
or impl
type.
In a fn item context, or in the T
in Box<T>
, once we see dyn
or impl
, there is only one valid interpretation (since we are not in a where clause list) -- . These are exactly the cases (I believe) where we use the grammatical nonterminal T0
. We can parse eagerly there.
In other contexts, there are multiple contending interpretations. We are aiming not to choose between them (hence "non-associative").
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nikomatsakis
Thanks for the detailed write up, this is better than just "lang team decided so".
I agree that consistency with fn f() -> A + B { ... }
is a good argument in favor of potentially swapping priorities for Fn
and +
in the future.
I'll implement the conservative "refuse to disambiguate" solution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This discussion should've probably been happening in rust-lang/rfcs#2250, because the RFC needs to be updated as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, I'll copy my post over there.
|
||
type A = Fn() -> impl A + B; // OK, interpreted as `(Fn() -> impl A) + B` | ||
type A = Fn() -> dyn A + B; // OK, interpreted as `(Fn() -> dyn A) + B` | ||
type A = Fn() -> A + B; // OK, interpreted as `(Fn() -> A) + B` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Obviously we can't alter this though.
`+` is still disallowed in function types and function-like traits
732a623
to
f57ea7c
Compare
Updated. |
@bors r+ Looks nice! |
📌 Commit f57ea7c has been approved by |
syntax: Lower priority of `+` in `impl Trait`/`dyn Trait` Now you have to write `Fn() -> (impl A + B)` instead of `Fn() -> impl A + B`, this is consistent with priority of `+` in trait objects (`Fn() -> A + B` means `(Fn() -> A) + B`). To make this viable I changed the syntax to also permit `+` in return types in function declarations ``` fn f() -> dyn A + B { ... } // OK, don't have to write `-> (dyn A + B)` // This is acceptable, because `dyn A + B` here is an isolated type and // not part of a larger type with various operator priorities in play // like `dyn A + B` in `Fn() -> dyn A + B` despite syntax similarities. ``` but you still have to use `-> (dyn A + B)` in function types and function-like trait object types (see this PR's tests for examples). This can be a breaking change for code using `impl Trait` on nightly. The thing that is most likely to break is `&impl A + B`, it needs to be rewritten as `&(impl A + B)`. cc #34511 #44662 rust-lang/rfcs#438
💔 Test failed - status-appveyor |
syntax: Lower priority of `+` in `impl Trait`/`dyn Trait` Now you have to write `Fn() -> (impl A + B)` instead of `Fn() -> impl A + B`, this is consistent with priority of `+` in trait objects (`Fn() -> A + B` means `(Fn() -> A) + B`). To make this viable I changed the syntax to also permit `+` in return types in function declarations ``` fn f() -> dyn A + B { ... } // OK, don't have to write `-> (dyn A + B)` // This is acceptable, because `dyn A + B` here is an isolated type and // not part of a larger type with various operator priorities in play // like `dyn A + B` in `Fn() -> dyn A + B` despite syntax similarities. ``` but you still have to use `-> (dyn A + B)` in function types and function-like trait object types (see this PR's tests for examples). This can be a breaking change for code using `impl Trait` on nightly. The thing that is most likely to break is `&impl A + B`, it needs to be rewritten as `&(impl A + B)`. cc #34511 #44662 rust-lang/rfcs#438
☀️ Test successful - status-appveyor, status-travis |
Now you have to write
Fn() -> (impl A + B)
instead ofFn() -> impl A + B
, this is consistent with priority of+
in trait objects (Fn() -> A + B
means(Fn() -> A) + B
).To make this viable I changed the syntax to also permit
+
in return types in function declarationsbut you still have to use
-> (dyn A + B)
in function types and function-like trait object types (see this PR's tests for examples).This can be a breaking change for code using
impl Trait
on nightly. The thing that is most likely to break is&impl A + B
, it needs to be rewritten as&(impl A + B)
.cc #34511 #44662 rust-lang/rfcs#438