Better Implicit Search Errors: problematic cases wanted!

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.

1 Like

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.

3 Likes

Rust-like compiler assistance with implicits

In Principles for Implicits in Scala 3 @odersky wrote:

Since Rust was given as an example of universally acclaimed implementation of typeclasses I think I need to remind how Rust compiler makes typeclasses programmer friendly. It does that through helpful error messages and also that’s what this topic is about.

List of Rust examples I’ve given:




In short, given code like subject.extension_provided_though_typeclass(args) Rust compiler:

  • scans the whole classpath to search for typeclasses containing the desired method and prints a numbered list of them for the user (up to a few possibilities). Usually the first one is the good one, so maybe some prioritizing by usefullness is done (I don’t remember how, because I’ve not done Rust programming for over a year or so)
  • doesn’t seem to look for typeclass instances dependencies as it doesn’t seem to look for typeclass instances at all (only for interfaces) - this probably is because no orphan typeclass instances are allowed in Rust, so all the instances definitions are either next to subject type definition or next to typeclass (i.e. Rust’s trait).
  • helps to resolve ambiguities. If extension_provided_though_typeclass is present in multiple imported typeclasses then Rust compiler lists all of them and shows the user how he can disambiguate the call

By allowing compiler to give a list of suggestions, some of them can be false positives but the list as a whole can still be useful. It’s better to try few suggestions that are automatically given than to figure out everything by yourself because compiler couldn’t decide on a single suggestion and because of that it stayed silent.

It seems that compilation error messages in Dotty finally have some of the usefulness that Rust compiler provides.

Generally the more suggestions the better, provided the message is still readable. However, if that makes compilation much longer then maybe it’s better to make two levels of implicits search depth. For sure there should be an option to disable the whole mechanism for CI/CD builds, which should stay fast (i.e. make it opt out).

Also a general suggestion regarding errors: In Rust errors have codes and that makes them much easier to google, e.g. try googlling “rust E0119”. Rust website also has https://doc.rust-lang.org/error-index.html and that’s where IDE automatically points to when error code appears in the compilation logs. An added bonus is that the error message for a given error code can be frequently changed and that wouldn’t prevent googling it.

3 Likes

I’m not sure I agree. The key question is, does this make compilation longer only in the presence of errors? That’s what I would expect – that performance is largely unchanged in a clean compile, but generating error messages is slower.

In that case, I’d be in favor of always having the clearer messages, even in CI/CD – perhaps even moreso there, since debugging problems in the non-interactive environment is often harder, and delays that are irritating in a command-line compile are often just minor noise in a CI environment.

4 Likes

I completely agree that getting more helpful diagnostics must be the #1 priority.

4 Likes