Skip to content

Commit cd6f781

Browse files
authored
Merge pull request #1136 from scalacenter/implicits-error-messages
Add blog article about import suggestions
2 parents 9739cf4 + bde94a1 commit cd6f781

File tree

2 files changed

+324
-0
lines changed

2 files changed

+324
-0
lines changed
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
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+
![cloud of words](/resources/img/blog/implicit-challenge.png)
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
255 KB
Loading

0 commit comments

Comments
 (0)