|
| 1 | +--- |
| 2 | +layout: blog-detail |
| 3 | +post-type: blog |
| 4 | +by: Julien Richard-Foy, Scala Center |
| 5 | +title: Import Suggestions in Scala 3 |
| 6 | +--- |
| 7 | + |
| 8 | +Implicits let the compiler “write” significant parts of a program for you. |
| 9 | +For instance, the compiler can summon JSON serializers and deserializers for |
| 10 | +a complete type hierarchy. |
| 11 | + |
| 12 | +However, working with implicits can be a difficult experience. Thankfully, the |
| 13 | +Scala 3 compiler dramatically |
| 14 | +improves the quality of the error messages shown in case of missing implicits so |
| 15 | +that it is easier to see _where_ an implicit argument could not be inferred |
| 16 | +by the compiler, and _how_ to fix the problem. |
| 17 | + |
| 18 | +This article shows these improvements in action in concrete examples of code. |
| 19 | + |
| 20 | +## Motivation |
| 21 | + |
| 22 | +In the [2018 Scala developer survey], the word “implicit” showed |
| 23 | +up in the question “In learning Scala, what was the biggest challenge you |
| 24 | +faced?” |
| 25 | + |
| 26 | + |
| 27 | + |
| 28 | +We also saw that 35% of the respondents of the [2019 developer survey] signaled |
| 29 | +that dealing with missing implicits was a main pain point in their daily |
| 30 | +workflow. Furthermore, they signaled that the two most painful issues they had |
| 31 | +when working with implicits were “finding which parameters have been inferred”, |
| 32 | +and “fixing 'implicit not found' errors”. Last but not least, the |
| 33 | +word that was most mentioned by the respondents to describe their other pain points |
| 34 | +related to implicits is the word “import”. |
| 35 | + |
| 36 | +A few months ago, Jamie Thompson engaged a [discussion with the community] to |
| 37 | +understand better the problem. We identified that “conditional” implicits |
| 38 | +were probably involved in most of the issues. Conditional implicits are |
| 39 | +implicit definitions that themselves take implicit parameters. For instance, |
| 40 | +an implicit `Ordering[List[A]]` instance requiring that there is an implicit |
| 41 | +`Ordering[A]` instance: |
| 42 | + |
| 43 | +~~~ |
| 44 | +implicit def orderingList[A](implicit orderingA: Ordering[A]): Ordering[List[A]] |
| 45 | +~~~ |
| 46 | + |
| 47 | +Consider what happens when you call a method that requires an implicit |
| 48 | +`Ordering[List[Int]]`. The compiler searches for such an implicit definition |
| 49 | +and finds that the implicit definition `orderingList` could be a good |
| 50 | +candidate provided that there is an implicit instance of type `Ordering[Int]`. |
| 51 | +The compiler searches for such an implicit definition (which it finds in the |
| 52 | +`Ordering` companion object) and summons the initial `Ordering[List[Int]]` |
| 53 | +implicit argument by supplying the `Ordering[Int]` instance to the implicit |
| 54 | +definition `orderingList`. In this example we have only two implicit |
| 55 | +definitions involved, but in practice conditional implicit definitions can |
| 56 | +form longer chains. |
| 57 | + |
| 58 | +Now, let’s have a look at what happens in Scala 2 if something fails somewhere |
| 59 | +in the chain. For example, when we call a method that requires an implicit |
| 60 | +`Ordering[List[Foo]]` but there is no implicit `Ordering[Foo]` instance: |
| 61 | + |
| 62 | +~~~ |
| 63 | +class Foo |
| 64 | +
|
| 65 | +List(List(new Foo)).sorted |
| 66 | +~~~ |
| 67 | + |
| 68 | +The Scala 2 compiler produces the following error: |
| 69 | + |
| 70 | +~~~ |
| 71 | +No implicit Ordering defined for List[Foo]. |
| 72 | +~~~ |
| 73 | + |
| 74 | +The error message says that no implicit `Ordering` instance for type |
| 75 | +`List[Foo]` could be found. However, this message is not very |
| 76 | +precise. The actual reason of the failure is that there was no implicit |
| 77 | +`Ordering` instance for type `Foo`. Because of that, no implicit `Ordering` |
| 78 | +instance for type `List[Foo]` could be summoned by the compiler. |
| 79 | + |
| 80 | +This is the first concrete problem we identified: error messages don’t |
| 81 | +tell precisely **where** in the chain was the missing implicit. |
| 82 | + |
| 83 | +The second problem we identified is that issues related to implicits are |
| 84 | +often due to missing imports, but finding **what** to import is hard. |
| 85 | + |
| 86 | +The next sections show how Scala 3 addresses both issues by providing more |
| 87 | +detailed error messages and actionable feedback. |
| 88 | + |
| 89 | +## Showing Where the Problem Is |
| 90 | + |
| 91 | +In case an implicit argument could not be found in a chain of implicit definitions, |
| 92 | +the Scala 3 compiler now shows the complete chain it could build until an argument |
| 93 | +could not be found. Here is an example that mimics the `Ordering[List[A]]` problem |
| 94 | +mentioned above: |
| 95 | + |
| 96 | +~~~ |
| 97 | +// `Order` type class definition, similar to the `Ordering` type class of |
| 98 | +// the standard library |
| 99 | +trait Order[A] { |
| 100 | + def compare(a1: A, a2: A): Int |
| 101 | +} |
| 102 | +
|
| 103 | +object Order { |
| 104 | + // Provides an implicit instance of type `Order[List[A]]` under the condition |
| 105 | + // that there is an implicit instance of type `Order[A]` |
| 106 | + implicit def orderList[A](implicit orderA: Order[A]): Order[List[A]] = ??? |
| 107 | +} |
| 108 | +
|
| 109 | +// Sorts a `list` of elements of type `A` with their implicit `order` relation |
| 110 | +def sort[A](list: List[A])(implicit order: Order[A]): List[A] = ??? |
| 111 | +
|
| 112 | +// A class `Foo` |
| 113 | +class Foo |
| 114 | +
|
| 115 | +// Let’s try to sort a `List[List[Foo]]` |
| 116 | +sort(List(List(new Foo))) |
| 117 | +~~~ |
| 118 | + |
| 119 | +The Scala 3 compiler gives the following error message: |
| 120 | + |
| 121 | +~~~ |
| 122 | +Error: |
| 123 | +| sort(List(List(new Foo))) |
| 124 | +| ^ |
| 125 | +|no implicit argument of type Order[List[Foo]] was found for parameter order of method sort. |
| 126 | +|I found: |
| 127 | +| |
| 128 | +| Order.orderList[A](/* missing */implicitly[Order[Foo]]) |
| 129 | +| |
| 130 | +|But no implicit values were found that match type Order[Foo]. |
| 131 | +~~~ |
| 132 | + |
| 133 | +The error message now shows how far the compiler went by chaining |
| 134 | +implicit definitions, and where it eventually stopped because an |
| 135 | +implicit argument could not be found. In our case, we see that the |
| 136 | +compiler tried the definition `orderList` but then didn’t find an |
| 137 | +implicit `Order[Foo]`. So, we know that to fix the problem we need |
| 138 | +to implement an implicit `Order[Foo]`. |
| 139 | + |
| 140 | +> For the record, the idea of showing the complete chain of implicits was |
| 141 | +> pioneered by Torsten Schmits in the [splain] compiler plugin, which is |
| 142 | +> available in Scala 2. |
| 143 | +
|
| 144 | +## Suggesting How to Fix the Problem |
| 145 | + |
| 146 | +In case the missing implicit arguments are defined somewhere but need to |
| 147 | +be imported, the Scala 3 compiler suggests to you `import` clauses that might |
| 148 | +fix the problem. |
| 149 | + |
| 150 | +Here is an example that illustrates this: |
| 151 | + |
| 152 | +~~~ |
| 153 | +// A class `Bar` |
| 154 | +class Bar |
| 155 | +
|
| 156 | +// An implicit `Order[Bar]` |
| 157 | +// (note that it is _not_ in the `Bar` companion object) |
| 158 | +object Implicits { |
| 159 | + implicit def orderBar: Order[Bar] = ??? |
| 160 | +} |
| 161 | +
|
| 162 | +// Let’s try to sort a `List[Bar]` |
| 163 | +sort(List(new Bar)) |
| 164 | +~~~ |
| 165 | + |
| 166 | +The compiler produces the following error: |
| 167 | + |
| 168 | +~~~ |
| 169 | +Error: |
| 170 | +| sort(List(new Bar)) |
| 171 | +| ^ |
| 172 | +|no implicit argument of type Order[Bar] was found for parameter order of method sort |
| 173 | +| |
| 174 | +|The following import might fix the problem: |
| 175 | +| |
| 176 | +| import Implicits.orderBar |
| 177 | +~~~ |
| 178 | + |
| 179 | +Instead of just reporting that an implicit argument was not found, the Scala 3 compiler |
| 180 | +looks for implicit definitions that could have provided the missing argument. In our case, |
| 181 | +the compiler suggests importing `Implicits.orderBar`, which does fix the compilation error. |
| 182 | + |
| 183 | +## A More Sophisticated Example |
| 184 | + |
| 185 | +An iconic example is the operation `traverse` from the library [cats]. This |
| 186 | +operation is defined as a _conditional extension method_ on any type `F[A]` |
| 187 | +for which there exists an implicit `Traverse[F]` instance. The operation |
| 188 | +takes a function `A => G[B]` and an implicit parameter of type `Applicative[G]`. |
| 189 | + |
| 190 | +In practice, this very generic operation is used in various specific contexts. For |
| 191 | +instance, to turn a list of validation results into a single validation result |
| 192 | +containing a list, or to turn an optional asynchronous result into an |
| 193 | +asynchronous optional result. However, because it is a conditional extension method, |
| 194 | +and because it takes an implicit parameter, finding the correct imports to make it work |
| 195 | +can be difficult. |
| 196 | + |
| 197 | +You don’t need to be familiar with the type classes `Traverse` and `Applicative` to |
| 198 | +understand the remaining of this article. There are only two things to know |
| 199 | +about the operation `traverse`: |
| 200 | + |
| 201 | +1. it is available on a value of type `List[A]` if there is an |
| 202 | + implicit instance of type `Traverse[List]` (it is a _conditional_ extension method), |
| 203 | +2. the operation itself takes an implicit parameter of type `Applicative`. |
| 204 | + |
| 205 | +This can be modeled as follows in Scala 3, using [extension methods]: |
| 206 | + |
| 207 | +~~~ |
| 208 | +// The `Traverse` type class, which provides a `traverse` operation as an extension method |
| 209 | +trait Traverse[F[_]] { |
| 210 | + def [G[_], A, B](fa: F[A]).traverse(f: A => G[B])(implicit applicative: Applicative[G]): G[B] |
| 211 | +} |
| 212 | +
|
| 213 | +// The applicative type class (its actual definition does not matter for the example) |
| 214 | +trait Applicative[F[_]] |
| 215 | +~~~ |
| 216 | + |
| 217 | +Let’s assume that a given instance of type `Traverse[List]` and a given instance |
| 218 | +of type `Applicative[Option]` are defined in an object `Givens` (given instances |
| 219 | +are the new way to define implicit instances in Scala 3): |
| 220 | + |
| 221 | +~~~ |
| 222 | +object Givens { |
| 223 | + given traverseList as Traverse[List] = ??? |
| 224 | + given applicativeOption as Applicative[Option] = ??? |
| 225 | +} |
| 226 | +~~~ |
| 227 | + |
| 228 | +Now that we have set the context, let’s see a concrete example of use of `traverse`. |
| 229 | + |
| 230 | +First, consider a function `parseUser`, that parses a `User` |
| 231 | +from a `String` (e.g., containing a JSON object): |
| 232 | + |
| 233 | +~~~ |
| 234 | +def parseUser(string: String): Option[User] |
| 235 | +~~~ |
| 236 | + |
| 237 | +The return type of the function is `Option[User]`, which can represent a |
| 238 | +parsing failure with `None`, or a parsing success with `Some`. |
| 239 | + |
| 240 | +We can use the operation `traverse` and the function `parseUser` (which |
| 241 | +parses _one_ user) to implement a function `parseUsers`, which parses |
| 242 | +a _list_ of users. The signature of this function is the following: |
| 243 | + |
| 244 | +~~~ |
| 245 | +def parseUsers(strings: List[String]): Option[List[User]] |
| 246 | +~~~ |
| 247 | + |
| 248 | +Again, the result type is `Option[List[User]]` so that a parsing failure can |
| 249 | +be represented (it returns `None` if any of the strings failed to be parsed). |
| 250 | + |
| 251 | +The function can be implemented as follows: |
| 252 | + |
| 253 | +~~~ |
| 254 | +def parseUsers(strings: List[String]): Option[List[User]] = |
| 255 | + strings.traverse(parseUser) |
| 256 | +~~~ |
| 257 | + |
| 258 | +However, if we try to compile this code with Scala 2 we get the following |
| 259 | +error: |
| 260 | + |
| 261 | +~~~ |
| 262 | +value traverse is not a member of List[String] |
| 263 | +did you mean reverse? |
| 264 | +~~~ |
| 265 | + |
| 266 | +The error message doesn’t help to find a solution. |
| 267 | + |
| 268 | +Compiling with Scala 3, on the other hand, provides much better assistance: |
| 269 | + |
| 270 | +~~~ |
| 271 | +[E008] Not Found Error: |
| 272 | +| strings.traverse(parseUser) |
| 273 | +| ^^^^^^^^^^^^^^^^ |
| 274 | +|value traverse is not a member of List[String], but could be made available as an extension method. |
| 275 | +| |
| 276 | +|The following import might make progress towards fixing the problem: |
| 277 | +| |
| 278 | +| import Givens.traverseList |
| 279 | +~~~ |
| 280 | + |
| 281 | +Let’s apply the suggestion and import `Givens.traverseList`. Now, the compiler |
| 282 | +provides the following error: |
| 283 | + |
| 284 | +~~~ |
| 285 | +Error: |
| 286 | +| strings.traverse(parseUser) |
| 287 | +| ^ |
| 288 | +|no implicit argument of type Applicative[Option] was found for parameter applicative of method traverse in trait Traverse |
| 289 | +| |
| 290 | +|The following import might fix the problem: |
| 291 | +| |
| 292 | +| import Givens.applicativeOption |
| 293 | +~~~ |
| 294 | + |
| 295 | +If we apply the new suggestion (importing `Givens.applicativeOption`) our |
| 296 | +program compiles! |
| 297 | + |
| 298 | +The Scala 3 compiler first suggested importing `Givens.traverseList`, so |
| 299 | +that the extension method `traverse` becomes available. Then, it suggested |
| 300 | +importing `Givens.applicativeOption`, which was required to call the `traverse` |
| 301 | +operation. |
| 302 | + |
| 303 | +## Summary |
| 304 | + |
| 305 | +Dealing with “implicit not found” errors in Scala 2 can be difficult, in particular |
| 306 | +because developers don’t see precisely which implicit argument could not be found |
| 307 | +in a chain of implicit definitions, or because they don’t know what are the required |
| 308 | +imports to add to their program. |
| 309 | + |
| 310 | +Scala 3 addresses these two pain points by: |
| 311 | + |
| 312 | +- providing more precise error messages, showing exactly which implicit argument |
| 313 | + could not be found in a chain of implicit definitions, |
| 314 | +- providing actionable feedback, suggesting `import` clauses that might provide |
| 315 | + the missing implicits. |
| 316 | + |
| 317 | +You can already try this feature in [Dotty 0.24.0-RC1](https://dotty.epfl.ch/docs/usage/getting-started.html). |
| 318 | + |
| 319 | +[2018 Scala developer survey]: https://contributors.scala-lang.org/t/preliminary-developer-survey-results/2681 |
| 320 | +[2019 developer survey]: https://scalacenter.github.io/scala-developer-survey-2019/#what-are-the-main-pain-points-in-your-daily-workflow |
| 321 | +[discussion with the community]: https://contributors.scala-lang.org/t/better-implicit-search-errors-problematic-cases-wanted/3587 |
| 322 | +[splain]: https://github.com/tek/splain |
| 323 | +[cats]: https://github.com/typelevel/cats |
| 324 | +[extension methods]: https://dotty.epfl.ch/docs/reference/contextual/extension-methods.html |
0 commit comments