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

Time:09-10

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?

CodePudding user response:

Generalized type constraints are not some magic hints for the type checker for every type. They are just implicit conversions masked as implicit parameters. For example:

(implicit ev: job.T <:< Product) translates to the compiler like this: "if you give me a value of the specific job.T, I can treat it as a value of the more general Product, because you provided me a conversion (evidence or proof) from job.T to Product". It does not tell him that job.T is a subtype of Product. Since you have no value of job.T in the method, ev is never used, hence the warning you mentioned.

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 type you want, 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

The 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 fixes the earlier problem.

I tested both on Scala 2.13.8 and worked.

CodePudding user response:

Expanding on Alin's answer, you may also use a type alias to express the same thing like this:

type JobProduct[K, P <: Product] = Job[K] { type T = P }

// Here I personally prefer to use a type parameter rather than an existential
// since I have had troubles with those, but if you don't find issues you may just use
// JobProdut[_, P] instead and remove the K type parameter.
def encoder[K, P <: Product](job: JobProduct[K, P]): Encoder[P] =
  new Encoder[P]()

This approach may be more readable to newcomers and allows reuse; however, is essentially the same as what Alin did.

  • Related