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.