I'm having some problems with typeclasses in Scala and more precisely with errors at compile time when a typeclass instance cannot be found. Let's say that I have a typeclass TC
and an object B[C]
that has an instance of TC
only if C
has an instance of TC
. In Scala this can be written implicit def btc[C](implicit ctc: TC[C]) = new TC[B[C]] { ... }
. When you need a TC[B[C]]
for a C
for which scalac cannot find an instance TC[C]
, scalac will give an error saying that it cannot find a TC[B[C]]
. While this is true, the error message is missing the reason why scalac cannot find TC[B[C]]
, that is that it cannot find TC[C]
.
To illustrate the problem, I decided to make a small toy example where TC
is PrettyPrintable
, C
is Unit
and B
is Option
:
import scala.annotation.implicitNotFound
@implicitNotFound("Cannot pretty print instances of the type ${T}")
trait PrettyPrintable[T] {
def prettyPrint(t: T): String
}
object PrettyPrintable {
def apply[T](implicit pp: PrettyPrintable[T]): PrettyPrintable[T] = pp
implicit def optPP[T](implicit opp: PrettyPrintable[T]) = new PrettyPrintable[Option[T]] {
override def prettyPrint(ot: Option[T]): String = ot.fold("")(opp.prettyPrint)
}
}
object Main extends App {
println(PrettyPrintable[Option[Unit]].prettyPrint(None)) // error
}
If I run the application, I get the error:
[error] /home/rief/prog/scala/implicitNotFoundTest/Main.scala:24: Cannot pretty print instances of the type Option[Unit]
[error] println(PrettyPrintable[Option[Unit]].prettyPrint(None))
This is of course true: scalac can't find a pretty print instance for type Option[Unit]
. The problem is that the error itself is not very helpful because the point is not that scalac can't find an instance of the type class but more why it cannot find it. In this case, the reason why Option[Unit]
doesn't have a pretty print instance is because Unit
doesn't have a pretty print instance but for more complex cases this can be a nightmare.
My question is: is it possible to make errors on implicit not found more precise?
That would be nice, but I think the problem you want to solve is a bit more complicated than it seems at first blush. In this example, I've added some more implicit vals and defs for PrettyPrintable
:
import scala.annotation.implicitNotFound
@implicitNotFound("Cannot pretty print instances of the type ${T}")
trait PrettyPrintable[T] {
def prettyPrint(t: T): String
}
object PrettyPrintable {
def apply[T](implicit pp: PrettyPrintable[T]): PrettyPrintable[T] = pp
implicit val intPP = new PrettyPrintable[Int] {
override def prettyPrint(i: Int): String = s"== $i =="
}
implicit def optPP[T](implicit opp: PrettyPrintable[T]) = new PrettyPrintable[Option[T]] {
override def prettyPrint(ot: Option[T]): String = s"-- ${ot.map(opp.prettyPrint)} --"
}
implicit def pairPP[T, U](implicit tpp: PrettyPrintable[T], upp: PrettyPrintable[U]) =
new PrettyPrintable[(T, U)] {
override def prettyPrint(pair: (T, U)): String =
s"[[[ ${tpp.prettyPrint(pair._1)} >>> ${upp.prettyPrint(pair._2)} ]]]"
}
}
object Main extends App {
println(PrettyPrintable[Int].prettyPrint(6)) // prints == 6 ==
println(PrettyPrintable[Option[Int]].prettyPrint(None)) // prints -- None --
println(PrettyPrintable[Option[Int]].prettyPrint(Some(6))) // prints -- Some(== 6 ==) --
println(PrettyPrintable[(Int,Int)].prettyPrint((6 -> 7))) // prints [[[ == 6 == >>> == 7 == ]]]
println(PrettyPrintable[(Float,Long)].prettyPrint((6F -> 7L))) // error
}
The last line produces the following compiler error, analogous to the error from your example:
Cannot pretty print instances of the type (Float, Long)
In your case, it is easy to cast the blame for not finding an implicit PrettyPrintable[Option[Unit]]
on not finding an implicit PrettyPrintable[Unit]
. But in the case of the pair, would you want to blame not finding an implicit PrettyPrintable[(Float, Long)]
on not finding an implicit PrettyPrintable[Float]
, not finding an implicit PrettyPrintable[Long]
, or both?
Note that the root cause of the implicit resolution failure might not even have the same shape as the original. For instance, consider adding the following two implicit defs:
implicit def i2ppf(implicit i: Int) = new PrettyPrintable[Float] {
override def prettyPrint(f: Float): String = s"xx $f xx"
}
implicit def s2ppf(implicit s: String) = new PrettyPrintable[Float] {
override def prettyPrint(f: Float): String = s"xx $f xx"
}
Now, is the root cause not finding an implicit Int
, or not finding an implicit String
? The actual answer is very complicated. Something like this:
PrettyPrintable[(Float,Long)]
because:
PrettyPrintable[Float]
and PrettyPrintable[Long]
PrettyPrintable[Float]
because:
Int
, ANDString
PrettyPrintable[Long]
because:
In answer to your question, "is it possible to make errors on implicit not found more precise?", I think that would involve providing you some kind of programmatic access to a wide tree of possibilities for you to construct an error message more precisely in the general case. Just designing a decent API for the job would be a pretty impressive undertaking, let alone an implementation of that API.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With