Skip to content

Commit 2a992b7

Browse files
committed
Add blog article about import suggestions
1 parent 21146e0 commit 2a992b7

File tree

2 files changed

+318
-0
lines changed

2 files changed

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

0 commit comments

Comments
 (0)