Reputation: 1358
The compiler is failing to choose the correct implicit conversion method when that conversion occurs within an implicit class declaration. In the example below, I have a Foo[T]
class and an implicit Helper
class that takes a Foo
and provides a print
method. That print method calls show
, which is itself a method provided by an implicit conversion on Foo
.
The catch is that there are two possible conversions that provide show
: one converts Foo[T]
to a Bar[T]
and the other converts Foo[Array[T]]
to a BarArray[T]
. The idea is that when we have a Foo
that contains an array, we want to apply the more specific BarArray
conversion. As far as I understand, the compiler chooses the conversion with the most specific type first.
This works in normal contexts as shown in the example below, but breaks within the context of the print
method in the implicit Helper
class. There, the same show
method is called and therefore I would expect the same conversions should be applied. However, in this context the compiler always chooses the Bar
conversion, even when it has a Foo[Array[T]]
and should choose the BarArray
conversion.
What is going wrong?
Minimal failing code example:
package scratch
import scala.language.implicitConversions
class Foo[T](val value: T) {}
object Foo {
implicit def fooToBar[T](foo: Foo[T]): Bar[T] = {
new Bar(foo.value)
}
implicit def fooArrayToBarArray[T](foo: Foo[Array[T]]): BarArray[T] = {
new BarArray(foo.value)
}
}
class Bar[T](val value: T) {
def show(): String = {
s"Bar($value)"
}
}
class BarArray[T](val value: Array[T]) {
def show(): String = {
value.map(v => s"Bar($v)").mkString(", ")
}
}
object Scratch extends App {
implicit class Helper[T](foo: Foo[T]) {
def print(): Unit = {
println(foo.show())
}
}
val foo0 = new Foo(123)
val foo1 = new Foo(Array(123, 456))
// conversions to Bar and BarArray work correctly here
println(foo0.show()) // Bar(123)
println(foo1.show()) // Bar(123), Bar(456)
// conversions called from within the implicit Helper class
// always choose the Bar conversion
foo0.print // Bar(123)
foo1.print // Bar([I@xxxxxxxx) <- should be Bar(123), Bar(456)
}
Versions:
Upvotes: 1
Views: 377
Reputation: 27525
Implicit resolution is "dispatched" in compile time so it only can access (type) information available to the compiler in particular place.
Here
val foo0 = new Foo(123)
val foo1 = new Foo(Array(123, 456))
// conversions to Bar and BarArray work correctly here
println(foo0.show()) // Bar(123)
println(foo1.show()) // Bar(123), Bar(456)
compiler infers types and implicits this way:
val foo0: Foo[Int] = new Foo(123)
val foo1: Foo[Array[Int]] = new Foo(Array(123, 456))
println(fooToBar(foo0).show()) // Bar(123)
// fooArrayToBarArray instead fooToBar because
// compiler knows that foo1: Foo[Array[Int]]
println(fooArrayToBarArray(foo1).show()) // Bar(123), Bar(456)
However here:
implicit class Helper[T](foo: Foo[T]) {
def print(): Unit = {
println(foo.show())
}
}
all compiler know is that foo: Foo[T]
. The same code has to be resolved now, there are no implicits incoming as arguments and the solution would have to be compiled once, and then type erasure kick in leaving hardcoded value of whatever implicit was best suited here. fooToBar
works perfectly. fooArrayToBarArray
expects proof that Foo's parameter is Array[T]
for some T
, which is nowhere to be found. By passing array here, you are forgetting about it, stripping compiler from any possibility to use array-specific implementation.
That is why @LuisMiguelMejíaSuárez suggested type classes:
// type class
trait FooPrinter[A] {
def show[A](foo: Foo[A]): String
def print[A](foo: Foo[A]): Unit = println(show(foo))
}
object FooPrinter {
// convenient summon method
def apply[A](implicit printer: FooPrinter[A]): FooPrinter[A] = printer
}
class Foo[T](val value: T)
// making sure that arrayPrinter takes precedence over tPrinter
// if both match requirements
object Foo extends FooLowPriorityImplicits {
implicit def arrayPrinter[T]: FooPrinter[Array[T]] =
_.map(v => s"Bar($v)").mkString(", ")
}
trait FooLowPriorityImplicits {
implicit def tPrinter[T]: FooPrinter[T] = v => s"Bar($v)"
}
implicit class Helper[T](private val foo: Foo[T]) extends AnyVal {
// requiring type class and summoning it using summon method
def print(implicit fp: FooPrinter[T]): Unit = FooPrinter[T].print(foo)
}
val foo0 = new Foo(123)
val foo1 = new Foo(Array(123, 456))
foo0.print
foo1.print
This way Helper
will not have to pick one implicit and "hardcode" it, because it will be passed to at as an argument:
new Helper(foo0).print(tPrinter)
new Helper(foo1).print(arrayPrinter)
though conveniently for us it will be done by the compiler. In your example no such communication between outside of Helper
and its inside happens, so whatever is resolved there is applied to everything that is passed.
Upvotes: 5