Better Implicit Search Errors: problematic cases wanted!

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 Rust error codes index - Error codes index 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

3 posts were split to a new topic: Compile error in Swing code