|
| 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