scalagenericsimplicit-conversionimplicitimplicit-class

Scala selects wrong implicit conversion from within implicit class method


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:


Solution

  • 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.