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