Skip to content

Commit 15e4dfd

Browse files
authored
Merge pull request #2397 from sgrif/sg-do-not-recommend
Introduce `#[do_not_recommend]` to control errors for trait impls
2 parents 9ab4ae2 + 326b29a commit 15e4dfd

File tree

1 file changed

+246
-0
lines changed

1 file changed

+246
-0
lines changed

text/2397-do-not-recommend.md

+246
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
- Feature Name: `do_not_recommend`
2+
- Start Date: 2018-04-07
3+
- RFC PR: [rust-lang/rfcs#2397](https://github.com/rust-lang/rfcs/pull/2397)
4+
- Rust Issue: [rust-lang/rust#51992](https://github.com/rust-lang/rust/issues/51992)
5+
6+
# Summary
7+
[summary]: #summary
8+
9+
A new attribute can be placed on trait implementations: `#[do_not_recommend]`.
10+
This attribute will cause the compiler to never recommend this impl transitively
11+
as a way to implement another trait. For example, this would be placed on
12+
`impl<T: Iterator> IntoIterator for T`. The result of this is that when `T:
13+
IntoIterator` fails, the error message will only mention `IntoIterator`. It will
14+
not say "perhaps `Iterator` should be implemented?".
15+
16+
# Motivation
17+
[motivation]: #motivation
18+
19+
When a type fails to implement a trait, Rust has the wonderful behavior of
20+
looking at possible *other* trait impls which might cause the trait in question
21+
to be implemented. This is usually a good thing. For example, when using Diesel,
22+
this is why instead of telling you `SelectStatement<{30 page long type}>:
23+
ExecuteDsl is not satisifed`, it tells you `posts::id:
24+
SelectableExpression<users::table> is not satisifed`.
25+
26+
However, there are times where this behavior actually makes the resulting error
27+
more confusing. There are specific trait impls which almost always cause these
28+
error messages to be more confusing. These are usually (but not always) very
29+
broad blanket impls on traits with names like `IntoFoo` or `AsBar`. One such
30+
problem impl is `impl<T: Iterator> IntoIterator for T`.
31+
32+
## `IntoIterator` confusion
33+
34+
Let's look at the struggles of a hypothetical Python programmer who is getting
35+
into Rust for the first time. In Python, tuples are iterable. So our python
36+
programmer writes this code expecting it to work:
37+
38+
```rust
39+
for i in (1, 2, 3) {
40+
println!("{}", i);
41+
}
42+
```
43+
44+
They get the following error:
45+
46+
```
47+
error[E0277]: the trait bound `({integer}, {integer}, {integer}): std::iter::Iterator` is not satisfied
48+
--> src/main.rs:2:14
49+
|
50+
2 | for x in (1, 2, 3) {
51+
| ^^^^^^^^^ `({integer}, {integer}, {integer})` is not an iterator; maybe try calling `.iter()` or a similar method
52+
|
53+
```
54+
55+
This error message is particularly bad for a failed `IntoIterator` constraint.
56+
The only type in `std` which has a method called `iter` that doesn't implement
57+
`IntoIterator` is a fixed sized array. For all of those types, it's generally
58+
more idiomatic to just put an `&` in front of the value. And for this case,
59+
neither one would be helpful even if it worked, since our hero is likely
60+
expecting `x` to be `i32`, not `&i32`.
61+
62+
Following the advice of the error message, they try calling `.iter` on their
63+
tuple, and get a new error:
64+
65+
```
66+
error[E0599]: no method named `iter` found for type `({integer}, {integer}, {integer})` in the current scope
67+
--> src/main.rs:2:24
68+
|
69+
2 | for x in (1, 2, 3).iter() {
70+
|
71+
```
72+
73+
At this point they remember a friend telling them they could see all of the
74+
types that implement some trait in the docs. Tuples clearly aren't the type we
75+
need, so let's see if we can find the type we *do* need. The error has told us
76+
that we need to be looking at `Iterator`, so that's where we look in the docs.
77+
78+
The implementors section there is... less than helpful. Other than the type
79+
`Map` (which our Rust newbie might incorrectly assume is `HashMap`), nothing
80+
here looks helpful. It's mostly just weird types called `Iter` and weird
81+
nonsense like `RSplitN`. At this point there's no obvious path to resolution.
82+
83+
If we had pointed them at `IntoIterator` like we should have, then the
84+
implementors section... Well it actually wouldn't have been much more helpful,
85+
since it's mostly just spammed with every single possible size of fixed sized
86+
array. However, that's a completely separate problem, and at the very least vec
87+
and slice, the type they most likely needed to see, are at least *somewhere* on
88+
that page.
89+
90+
If nothing else, *in this particular case*, there was at least a note saying
91+
"required by `std::iter::IntoIterator::into_iter`". However, the tiny footnote
92+
at the bottom is not where most people look, and as we'll see later, is also not
93+
always there or helpful.
94+
95+
## Ecosystem Examples
96+
97+
Let's look at another example from outside the standard library. This is a
98+
problem Diesel has run into numerous times. The most common is with our
99+
`AsExpression` trait. Diesel has a trait called `Expression`, which represents a
100+
fragment of SQL with a known type. There is also a trait called `AsExpression`,
101+
which is used to convert -- for example -- a Rust string into a data structure
102+
representing a `TEXT` SQL expression. Unlike `IntoIterator`, where `Item` is an
103+
associated type, in this case `SqlType` is a type parameter.
104+
105+
This gets represented in the type system to prevent things like accidentally
106+
trying to compare a string with a text column. Problem code might look like
107+
this: `a_table::id.eq(1)`. However, the error message they get is not so
108+
helpful:
109+
110+
```
111+
error[E0277]: the trait bound `str: diesel::Expression` is not satisfied
112+
--> src/lib.rs:14:17
113+
|
114+
14 | a_table::id.eq("1");
115+
| ^^ the trait `diesel::Expression` is not implemented for `str`
116+
|
117+
= note: required because of the requirements on the impl of `diesel::Expression` for `&str`
118+
= note: required because of the requirements on the impl of `diesel::expression::AsExpression<diesel::sql_types::Integer>` for `&str`
119+
```
120+
121+
Even worse, since the body of `impl<T: Expression> AsExpression<T::SqlType> for
122+
T` implies that the conversion returns `Self`, rust will continue on assuming
123+
that `&str` is a type that appears in the final AST. This results in our less
124+
than helpful message being even further behind 8 different trait impls that
125+
would never be implemented for `&str` in the first place.
126+
127+
Once again, we do have this little foot note with the information we care about,
128+
but as soon as we introduce one more layer of indirection, that gets completely
129+
lost. For example, if that code were instead written as
130+
`a_table::table.find("1")`, the full output we see is going to be:
131+
132+
```
133+
error[E0277]: the trait bound `str: diesel::Expression` is not satisfied
134+
--> src/lib.rs:14:20
135+
|
136+
14 | a_table::table.find("1");
137+
| ^^^^ the trait `diesel::Expression` is not implemented for `str`
138+
|
139+
= note: required because of the requirements on the impl of `diesel::Expression` for `&str`
140+
= note: required because of the requirements on the impl of `diesel::Expression` for `diesel::expression::operators::Eq<a_table::columns::id, &str>`
141+
= note: required because of the requirements on the impl of `diesel::EqAll<&str>` for `a_table::columns::id`
142+
= note: required because of the requirements on the impl of `diesel::query_dsl::filter_dsl::FindDsl<&str>` for `a_table::table`
143+
144+
error[E0277]: the trait bound `str: diesel::expression::NonAggregate` is not satisfied
145+
--> src/lib.rs:14:20
146+
|
147+
14 | a_table::table.find("1");
148+
| ^^^^ the trait `diesel::expression::NonAggregate` is not implemented for `str`
149+
|
150+
= note: required because of the requirements on the impl of `diesel::expression::NonAggregate` for `&str`
151+
= note: required because of the requirements on the impl of `diesel::expression::NonAggregate` for `diesel::expression::operators::Eq<a_table::columns::id, &str>`
152+
= note: required because of the requirements on the impl of `diesel::query_dsl::filter_dsl::FilterDsl<diesel::expression::operators::Eq<a_table::columns::id, &str>>` for `diesel::query_builder::SelectStatement<a_table::table>`
153+
```
154+
155+
Nowhere in this output is the *actual* missing trait (`AsExpression`) mentioned,
156+
nor is the type parameter we care about (`sql_types::Integer`), which is *the
157+
most important piece of information* ever mentioned.
158+
159+
The final motivation for this attribute is actually to *help* Rust give
160+
transitive impls when it currently isn't. The only time Rust will recommend
161+
implementing trait `T` in order to get an implementation of trait `U` is if
162+
there is only one such impl which could potentially apply to your type that
163+
would result in that behavior.
164+
165+
For example, Diesel has to provide a special impl to insert more than one row at
166+
a time on SQLite, which doesn't have the keywords needed to safely do this in a
167+
single query. However, on older versions of Diesel, if there is something
168+
missing that causes that insert statement to not be valid, Rust will just give
169+
up because it doesn't know if you wanted the "normal way to insert a thing" impl
170+
to apply, or the "insert an iterator on SQLite" impl to apply. In the best case
171+
this would result in "`InsertStatement<{30 page type}>: ExecuteDsl<Sqlite>` is
172+
not satisfied", which is not helpful, but at least it's not actively misleading.
173+
In the worst case it would result in "`YourRandomStruct: Iterator` is not
174+
satisfied. Perhaps you need to implement it?" which is just complete nonsense.
175+
176+
With this annotation, Rust would know that it should *never* recommend the impl
177+
related to `Iterators`, and will always give diagnostics as if the "normal way
178+
to insert a thing" impl were the only one that existed.
179+
180+
# Guide-level explanation
181+
[guide-level-explanation]: #guide-level-explanation
182+
183+
Since the diagnostics around this RFC aren't ever mentioned in a guide, I'm not
184+
sure there would be a guide level explanation, but here goes:
185+
186+
Let's imagine you have the following traits:
187+
188+
```
189+
pub trait Foo {
190+
}
191+
192+
pub trait Bar {
193+
}
194+
195+
impl<T: Foo> Bar for T {
196+
}
197+
```
198+
199+
If you tried to call a function that expects `T: Bar` with a type that does not
200+
implement `Bar`, Rust will helpfully notice that if `T` implemented `Foo`, it
201+
would also implement `Bar`. Because of that, it will recommend that you
202+
implement `Foo` instead of `Bar`.
203+
204+
This is usually the desired behavior, but in some cases it can result in
205+
confusing error messages. Perhaps when a function expects `Bar` and it's not
206+
implemented, it would never make sense to implement `Foo` for that type. In this
207+
case, we can put `#[do_not_recommend]` above our impl, and Rust will *never*
208+
recommend implementing `Foo` as a way to get to `Bar`.
209+
210+
# Reference-level explanation
211+
[reference-level-explanation]: #reference-level-explanation
212+
213+
During trait resolution, Rust will attempt to lower a query like
214+
`IntoIterator(?T)` into a series of subqueries such as `IntoIterator(?T) :-
215+
Iterator(?T)`. If only one such subquery exists, it will be used for error
216+
diagnostics instead.
217+
218+
With this RFC, for the purposes of diagnostics only, impls annotated with
219+
`#[do_not_recommend]` will be treated as if they did not exist. This means that
220+
cases where there would have been one subquery will be treated as if there were
221+
0, and cases where there were 2 will be treated as if there were 1.
222+
223+
# Drawbacks
224+
[drawbacks]: #drawbacks
225+
226+
While this attribute only affects diagnostics, it is inherently tied to how
227+
trait resolution works. This could potentially complicate work happening on the
228+
trait system today (particularly with regards to chalk).
229+
230+
# Rationale and alternatives
231+
[alternatives]: #alternatives
232+
233+
- The vast majority of cases where this would be used are for traits and impls
234+
that look very similar to `Iterator` and `impl<T: Iterator> IntoIterator for
235+
T`. We could potentially instead try to improve the compiler's diagnostics
236+
without this attribute, to detect those cases.
237+
238+
# Prior art
239+
[prior-art]: #prior-art
240+
241+
The author is not aware of any prior art regarding this feature.
242+
243+
# Unresolved questions
244+
[unresolved]: #unresolved-questions
245+
246+
- What other names could we go with besides `#[do_not_recommend]`?

0 commit comments

Comments
 (0)