Search code examples
scalaimplicittype-parametertype-boundsself-type

Type parameter under self-type doesn't conform to upper bound despite evidence


I have a trait with a self-type annotation that has a type parameter. This trait is from a library and cannot be modified. I want to pass this trait to a function that will require an upper bound for the type parameter. For example, I have this code snippet:

sealed trait Job[K] { self =>
  type T
}

case class Encoder[T <: Product]()

def encoder(job: Job[_])(implicit ev: job.T <:< Product): Encoder[job.T] =
  new Encoder[job.T]()

This returns an error that Type argument job.T does not conform to upper bound Product and a warning that ev is never used. How should I design the encoder function?


Solution

  • Why it doesn't work?

    Your issue has nothing to do with the generalized type constraint. You can remove it and still get the same error. A generalized type constraint is used to constrain the type of arguments the method can receive.

    (implicit ev: job.T <:< Product) provides an evidence in scope that matches only if job.T <: Product, allowing only calls to the method with Job arguments where job.T <: Product. This is its purpose.

    Your issue is because the Encoder class has its type parameter T <: Product. The generalized type constraint does not treat the type job.T itself as a subtype of Product, as you expected. The evidence only applies to value arguments, not to the type itself, because this is how implicit conversions work.

    For example, assuming a value x of type job.T that can be passed to the method as an argument:

      def encoder(job: Job[_])(x: job.T)(implicit ev: job.T <:< Product): Unit = {
        val y: Product          = x // expands to: ev.apply(x) 
        val z: Encoder[Product] = new Encoder[job.T] // does not compile
      }
    

    The first line compiles because x is expanded to ev.apply(x), but the second one cannot be expanded, regardless if Encoder is covariant or not.

    First workaround

    One workaround you can do is this:

      def encoder[U <: Product](job: Job[_])(implicit ev: job.T <:< Product): Encoder[U] =
        new Encoder[U]()
    

    The problem with this is that while both type parameters U and T are subtypes of Product, this definition does not says much about the relation between them, and the compiler (and even Intellij) will not infer the correct resulting type, unless you specify it explicitly. For example:

      val myjob = new Job[Int] {
        type T = (Int, Int)
      }
    
      val myencoder: Encoder[Nothing]     = encoder(myjob) // infers type Nothing
      val myencoder2: Encoder[(Int, Int)] = encoder[(Int, Int)](myjob) // fix
    

    But why use job.T <:< Product if we already have U <: Product. We can instead use the =:= evidence to make sure their types are equal.

      def encoder[U <: Product](job: Job[_])(implicit ev: job.T =:= U): Encoder[U] =
        new Encoder[U]()
    

    Now the resulting type will be correctly inferred.

    Second workaround

    A shorter workaround is using a structural type instead:

      def encoder(job: Job[_] { type T <: Product }): Encoder[job.T] =
        new Encoder[job.T]()
    

    Which is not only cleaner (doesn't require a generalized type constraint), but also avoids the earlier problem.

    Both versions work on Scala 2.13.8.