Search code examples
scalaimplicit-conversiontype-parameter

Scala: Implicit evidence for class with type parameter


Here is a simple setup with two traits, a class with a covariant type parameter bounded by the previous traits, and a second class with a type parameter bounded by the other class. For both classes, a particular method is available (via implicit evidence) only if one of the two traits underlies the type parameter. This compiles fine:

trait Foo
trait ReadableFoo extends Foo {def field: Int}

case class Bar[+F <: Foo](foo: F) {
  def readField(implicit evidence: F <:< ReadableFoo) = foo.field
}

case class Grill[+F <: Foo, +B <: Bar[F]](bar: B) {
  def readField(implicit evidence: F <:< ReadableFoo) = bar.readField
}

However, since Bar is covariant in F, I shouldn't need the F parameter in Grill. I should just require that B is a subtype of Bar[ReadableFoo]. This, however, fails:

case class Grill[+B <: Bar[_]](bar: B) {
  def readField(implicit evidence: B <:< Bar[ReadableFoo]) = bar.readField
}

with the error:

error: Cannot prove that Any <:< this.ReadableFoo.
  def readField(implicit evidence: B <:< Bar[ReadableFoo]) = bar.readField

Why is the implicit evidence not being taken into account?


Solution

  • The call bar.readField is possible because the evidence instance <:< allows an implicit conversion from B to Bar[ReadableFoo].

    The problem I think that to call readField you need a successive evidence parameter F <:< ReadableFoo. So my guess is, the compiler doesn't fully substitute the type parameter of Bar in the first search stage of the implicit resolution (because to find readField, it just requires any Bar in the first place). And then it chokes on the second implicit resolution, because there is no form of 'backtracking' as far as I know.

    Anyway. The good thing is, you know more than the compiler and you can engage the conversion explicitly, either by using the apply method of <:<, or by using the helper method implicitly:

    case class Grill[+B <: Bar[_]](bar: B) {
      def readField(implicit evidence: B <:< Bar[ReadableFoo]) = evidence(bar).readField
    }
    
    case class Grill[+B <: Bar[_]](bar: B) {
      def readField(implicit evidence: B <:< Bar[ReadableFoo]) = 
        implicitly[Bar[ReadableFoo]](bar).readField
    }
    

    There is another possibility which might be the cleanest, as it doesn't rely on the implementation of <:< which might be a problem as @Kaito suggests:

    case class Grill[+B <: Bar[_]](bar: B) {
      def readField(implicit evidence: B <:< Bar[ReadableFoo]) =
         (bar: Bar[ReadableFoo]).readField
    }