Better Implicit Search Errors: problematic cases wanted!

We at the Scala Center are interested in improving the developer experience surrounding implicit values, (and later the new system in Scala 3,) in particular, cases where an implicit value can not be found, and the feedback that was given by the compiler is not useful.

We would like to build a base of examples to gain an understanding of what suggestions can be made to the programmer to help them find a correct implicit value in scope. So I am inviting you to give code examples from any recent Scala/Dotty version, and any error messages, and then also suggest what you wished the compiler would tell you, (or any other tooling you may use).

Again, we would like to see your idea of the perfect feedback from the compiler – what would make your experience so much easier when you see that compilation has failed due to a missing implicit.

Try keeping the code as simple and small as possible
Avoid relying on private dependencies
Avoid depending on macros (e.g., replace macros by dummy code with the same signature, protip: you can pass -Xprint:typer to scalac to see what your code looks like after type checking and macro expansion, and you can also pass -Xprint-types to see the type of every tree node).
To make examples easily portable, you can send a link from https://scastie.scala-lang.org (you can add all the dependencies you want in the “Build Settings” page).

Thank you for participating!
Jamie

7 Likes

While I was writing a (long!) reply to this thread to point out the fact that implicit definitions that are themselves parameterized by implicits are hard to use at a large scale, I realized that this problem seems to be already fixed in Dotty. I haven’t tried yet to play with it on more complex use cases involving inductive search, but the current state is already a huge improvement over Scala 2.

For the record, you can find my initial post below. If you have experienced other frustrating situations (which have not been solved by Dotty yet!), please add them to this thread so that we can be confident that the next major Scala version will address all our pain points regarding the usage of implicits!

I will illustrate the problem of implicits parameterized by implicits with an experience report using Akka Http (note that the problem is not specifically related to this library, it is just used as an example).

The following code snippet implements an HTTP route that responds to requests with the "bar" content:

import akka.http.scaladsl.server.Directives._

val route = 
  path("foo") {
    complete("bar")
  }

Now, let’s say that I want to return a JSON document containing a data type MyData, I add the following changes:

 import akka.http.scaladsl.server.Directives._
+import de.heikoseeberger.akkahttpplayjson.PlayJsonSupport._
+
+case class MyData(s: String)

 val route = 
   path("foo") {
-    complete("bar")
+    complete(MyData("bar"))
   }

Now I get the following compilation error:

type mismatch;
 found   : MyData
 required: akka.http.scaladsl.marshalling.ToResponseMarshallable

If I look at the API documentation for ToResponseMarshallable, I see no way to produce a value of that type. If I go to the companion object’s documentation, I can see that there is an implicit conversion for all type A to ToResponseMarshallable, given an implicit parameter of type ToResponseMarshaller[A]. I understand that one way of producing a ToMarshallable value would be to get this implicit conversion triggered, so probably there was no implicit ToResponseMarshaller[MyData] instance in scope. I now go to the documentation of ToResponseMarshaller, which is an alias to Marshaller. The companion object of Marshaller shows a lot of implicit definitions. I have to inspect them one by one until I find one that could be a good candidate. I can go quite far with this process, because some of these implicit definitions are themselves parameterize by other implicit values… But it seems that none of them are relevant to my case. I now have to look at the de.heikoseeberger.akkahttpplayjson.PlayJsonSupport object, which I have imported, and in which I can find an implicit marshaller member, which can provide a ToEntityMarshaller[MyData] given an implicit Writes[MyData]. I was initially looking for a ToResponseMarshaller[MyData], but maybe a ToEntityMarshaller[MyData] value can be used to produce a ToResponseMarshaller[MyData]? I go back to Akka Http documentation and eventually find that there is an implicit member that does just that. So, what’s wrong in my code? There is no implicit Writes[MyData]! Indeed, If I add it, it compiles:

 import akka.http.scaladsl.server.Directives._
+import play.api.libs.json.{Json, Writes}
 import de.heikoseeberger.akkahttpplayjson.PlayJsonSupport._

 case class MyData(s: String)

+object MyData {
+  implicit val jsonWrites: Writes[MyData] = Json.writes[MyData]
+}

 val route = 
   path("foo") {
     complete(MyData("bar"))
   }

You can play with this code online using scastie.

Take away points

One part of the problem here was that a “type mismatch” was reported by the compiler rather than an “implicit not found”. This could be easily fixed (I believe) by discouraging developers to rely on implicit conversions. Instead, the complete method could have taken an implicit parameter:

-def complete(m: => ToResponseMarshallable): StandardRoute
+def complete[A](m: => A)(implicit ToResponseMarshaller[A]): StandardRoute

So, this is mostly something that could be solved by educating developers, rather than modifying the compiler.

That being said, maybe the “type mismatch” error message could be expanded to mention that although there was an implicit conversion from MyData to ToResponseMarshallable, this one was not applicable because it required an implicit value of type ToResponseMarshaller[MyData], which was not found.

This brings us to the second part of the problem. Even if there was no implicit conversion involved in my case, the error message would just have been “no implicit value for ToResponseMarshaller[MyData]”, without further explanation.

Instead, I would like the compiler to tell me more precisely why there was no such implicit value, by listing all the potential candidates that have eventually failed.

As I’ve noted in the introduction, it seems that Dotty already implements this! As you can see in this scastie snippet, here is the error message produced by the Dotty compiler:

no implicit argument of type akka.http.scaladsl.marshalling.ToResponseMarshaller[MyData] was found for parameter _marshaller of method apply in object ToResponseMarshallable.
I found:

    akka.http.scaladsl.marshalling.Marshaller.liftMarshaller[T](
      de.heikoseeberger.akkahttpplayjson.PlayJsonSupport.marshaller[A](
        /* missing */implicitly[play.api.libs.json.Writes[_ >: T]]
      , $conforms[Nothing])
    )

But no implicit values were found that match type play.api.libs.json.Writes[_ >: T].

I’m happy to see that the Dotty compiler reports a useful error message in case of implicit not found. One potential improvement over the current state would be to do the same in case of implicit conversions that failed (if we remove the explicit conversion to ToResponseMarshallable, Dotty shows the same “type mismatch” error as Scala).

3 Likes

nice example :slight_smile:

In Scala 2.13 (and even in older versions) if the scope has multiple implicits available(one coming from an import and other defined in the same scope), the compiler does not return an ambiguous implicit error. The example can be found here. I expected that the compiler would report an ambiguous implicit resolution error but instead it reports value underscorize is not a member of String

3 Likes

That’s again an interaction between implicit conversions and implicit search. If we explicitly call the conversion to StringifyOps the compiler reports ambiguous implicits, as expected.

In Dotty, I expect such kinds of implicit conversions to not be necessary anymore because they will be replaced with extension methods.

Also, note that in Dotty your example does not cause an “ambiguous implicits” error because the imported implicit instance has a higher priority than the one defined at line 22: https://scastie.scala-lang.org/s7RAqVvwRSCaatoMeaZbdQ. Personally, I think that an ambiguity should have been reported in that case. Maybe @anatoliykmetyuk knows if that’s expected or not?

2 Likes

Thank you Julien for your reply. Yes, the explicit call to StringifyOps throws an ambiguous error. Let me just put my understanding here:

1 - Compiler sees that there is no method called underscorize for a String.
2 - It checks if the String can be converted to an Object that has underscorize method.
3 - While StringifyOps does have underscorize method but conversion to this class requires an implicit Stringify. Since there are ambiguous implicit, the compiler does not convert to StringifyOps.
4 - Since no other conversions are found, the compiler returns the error value underscorize is not a member of String.

I liked the dotty’s error message in your example. Does it make sense for Scala 2 to show similar error message for such case?

2 Likes

I would love for this to be the case, the current workflow to debug this is really tedious.

1 Like

Yes, that’s my understanding too.

Yes definitely! But we are at a point where maybe it’s better not to duplicate work between Scala 2 and Scala 3. That being said, we can ask @adriaanm if improving implicit search failure messages (see what Dotty does) is on the Scala 2 roadmap.

2 Likes

At work we have been bitten a few times by Play Json implicits existing & being found but not in the proper order. And thus Json parsing fails at runtime, when the wrongly ordered implicits should be used.

This is really annoying and not “typesafe” (it compiles yet breaks), if something could be done it would rock!

1 Like

@cluelessjoe In order to keep reports actionable, please provide concrete examples, with at least a problematic code snippet, and what you were hoping the compiler would tell you.

2 Likes

I’ve gathered feedback from a discussion I just had with a colleague. Recurring frustrations were “sometimes it works and you have no idea why”, and “sometimes it doesn’t work and you have no guidance on how to make it work”. A compelling example might be the traverse operation, which involves two type class instances and extension methods.

Here are minimal examples of use of traverse:

List(1, 2, 3).traverse(x => x.some) // res0: Option[List[Int]] = Some(List(1, 2, 3))
List(1, 2, 3).traverse(x => x.asRight[Throwable]) // res1: Either[Throwable, List[Int]] = Right(List(1, 2, 3))

They work by simply importing cats.implicits._ and enabling partial type unification.


However, if one wants to import only the necessary implicit values, for instance to reduce compilation times, the error messages might be difficult to understand.

Here is the error message we get if there is no import at all:

value traverse is not a member of List[Int]
did you mean reverse?

Dotty produces a similar error message.

Note that in Dotty, there is a new way of encoding extension methods, which does not rely on implicit conversions. The encoding would look like the following:

trait Applicative[F[_]]
trait Traverse[F[_]] {
  def (fa: F[A]) traverse[A, B, G[_] : Applicative](f: A => G[B]): G[F[B]]
}

Unfortunately, we still get the same error message (“value traverse is not a member of List[Int] - did you mean ints.reverse?”) when there is no given (implicit) instance of Traverse[List] in scope, at the application point of the traverse method.

Similarly, the problem in Scala 2 is that there is no implicit conversion in scope to provide the traverse operation. A possible improvement would be to search in the classpath for possible implicit conversions providing the missing member, and report a message like so:

value traverse is not a member of List[Int]
did you mean reverse?
traverse can be provided by one of the following implicit conversions:
  import cats.syntax.traverse.toTraverseOps
  import cats.syntax.all.toTraverseOps

However, searching for eligible implicit conversions might significantly increase compilation times. (Even though this search would be performed only in case of missing member, so it should not affect the happy path) If the cost of this feature is too important, it could be hidden behind a compilation flag.

Furthermore, it turns out that these implicit conversions take implicit parameters. This means that when the compiler performs the search, it should take implicit parameters into account and check that they are satisfied. In the case they are not satisfied, it should scan the classpath again to find eligible static implicit instances. This would lead to the folowing error message:

value traverse is not a member of List[Int]
did you mean reverse?
traverse can be provided by one of the following implicit conversions:
  import cats.syntax.traverse.toTraverseOps
    whose implicit parameter can be satisfied by one of the following imports:
      import cats.instances.list.catsStdInstancesForList
      import cats.instances.all.catsStdInstancesForList
  import cats.syntax.all.toTraverseOps
    whose implicit parameter can be satisfied by one of the following imports:
      import cats.instances.list.catsStdInstancesForList
      import cats.instances.all.catsStdInstancesForList

Now, let’s assume that we have added the following imports to correctly resolve the traverse, some and asRight members:

import cats.syntax.traverse._
import cats.syntax.option._
import cats.syntax.either._
import cats.instances.list._

We now get the following error message:

could not find implicit value for evidence parameter of type cats.Applicative[Option]

Dotty gives a similar message.

Again, a more helpful message could consist in suggesting an import (if the compiler can find such an import) providing the missing instance:

could not find implicit value for evidence parameter of type cats.Applicative[Option]
it can be provided by one of the following imports:
  import cats.instances.option.catsStdInstancesForOption
  import cats.instances.all.catsStdInstancesForOption

Hopefully, such a feature would address the “sometimes it doesn’t work and you have no guidance on how to make it work” problem.


For the “sometimes it works and you have no idea why”, as @bishabosha noted in the introductory post, we can use the -Xprint-typer compiler flag. However, this solution does not scale because the whole program is printed. Instead, I would suggest introducing a method inline def printTyper(expr: Any): expr.type = expr, which could be used to delimit the parts of the code that we want the compiler to print.

6 Likes

It would be helpful to get some sort of quantification of how much this would typically slow things down. My gut reaction is strongly in favor of just turning this feature on universally – the added compile time is probably always less than puzzling out what to do by hand would be – but obviously if this added minutes to the compile I’d have to think about that.

In a perfect world (and I have no idea if this is even remotely plausible in reality), in interactive environments it would be lovely to quickly get an “Errors found – searching for guidance” message, and then pay the time penalty of looking things up. People are generally more tolerant of delays when they are provided meaningful feedback of why it’s taking a long time. (And ideally, how much more remains to be done.)

I have a suspicion that this doesn’t match the most obvious one-error-at-a-time reporting structure, but I do wonder whether it might also speed things up significantly if it was possible to gather up all such errors, and search for them at once, at the end of the overall compile…

5 Likes

Here you are:

import org.scalatest.FunSuite
import play.api.libs.json.{Format, Json}

class ImplicitOrderingTest extends FunSuite {

  implicit val BarFormat: Format[Bar] = Json.format
  implicit val FooFormat: Format[Foo] = Json.format
  test("Writing Bar - right order: compile and crash at runtime") {

    assert(BarFormat.writes(Bar(Foo(1))).toString() === """{"f":{"i":1}}""")  //throws!!
  }

}

case class Foo(i: Int)

case class Bar(f: Foo)

This compiles but throws NPE at runtime. Ideally it shouldn’t compile, just like this doesn’t compile :

import org.scalatest.FunSuite
import play.api.libs.json.{Format, Json}

class ImplicitOrderingTest extends FunSuite {

  test("Writing Bar - right order: compile and crash at runtime") {
    implicit val BarFormat: Format[Bar] = Json.format
    implicit val FooFormat: Format[Foo] = Json.format

    assert(BarFormat.writes(Bar(Foo(1))).toString() === """{"f":{"i":1}}""")  
  }

}

case class Foo(i: Int)

case class Bar(f: Foo)  

The compiler error being “forward reference extends over definition of value BarFormat
[error] implicit val BarFormat: Format[Bar] = Json.format”

This seems to be related to initialization more than implicits. We can reproduce the problem without using implicits. In my case, I still get a “Reference to uninitialized value” warning for the version that should not compile. I’m not sure why it’s just a warning and not a compilation error.

Ok, I posted the sample in Improve forward reference handling? then.

I don’t know for the warning instead of the error, i haven’t -Xfatal-warnings activated (and i’m running on 2.12.8).

By the way, we were bitten (hard) yesterday by a collision of 2 implicit names, in Scala 2.12. It was really painful to figure out what was going wrong, the issue being a ClassCastException at runtime.

The compiler wasn’t helping at all: no warning about the same implicit names… Once more a case of Scala compiling yet breaking at runtime: really bad feeling IMHO.

Is there something planned to tackle this, whether in 2.* or Dotty?

Overall i’m a bit surprised since i see quite some possible better ways:

  • a compiler warning when there are 2 implicits with the same name in scope
  • a compiler flag in order not to compile in this case
  • in the compiled code, always use the fully qualified name hence avoiding the issue completely

As usual, i’m pretty clueless, so i’m most likely missing something. However having stuff compiling yet breaking really sucks and shouldn’t happen in a strongly type language like Scala!

cheers

That really sounds like a bug – the implicit value is selected at compile time, and if a different value is passed at runtime, something is wrong.

I’ve never heard of any bug remotely like it so my guess would be that the problem was not exactly as described. And otherwise a minimized reproduction would be really great to have.

2 Likes