E234: Variance in Specialized Traits Limitation

Emitted when a type parameter of an inline trait is defined both Specialized and contravariant, to remind the user that this comes with a reduction in expressivity.

Ordinary contravariance works because of erasure, but specialized traits have a special erasure which prevents some contravariance patterns involving Object, Any, AnyVal and AnyRef.

The following example illustrates the problem:

inline trait RecyclingBin[-T: Specialized]:
    def recycle(x: T) = println(s"Recycling ${x}")

def recycleAnInteger(bin: RecyclingBin[Int]) = 
    bin.recycle(100)

recycleAnInteger(new RecyclingBin[AnyVal]() {}) // RecyclingBin[AnyVal] can be interpreted as RecyclingBin[Int] due to contravariance
recycleAnInteger(new RecyclingBin[Any]() {})    // RecyclingBin[Any] can be interpreted as RecyclingBin[Int] due to contravariance

// Yet, this erases to:

def recycleAnInteger(bin: RecyclingBin$sp$Int) = 
    bin.recycle(100)

recycleAnInteger(new RecyclingBin() {}.asInstanceOf[RecyclingBin$sp$Int]) // RecyclingBin cannot be cast to RecyclingBin$sp$Int
recycleAnInteger(new RecyclingBin() {}.asInstanceOf[RecyclingBin$sp$Int]) // RecyclingBin cannot be cast to RecyclingBin$sp$Int
  • RecyclingBin[Any], RecyclingBin[AnyVal] may not be passed to RecyclingBin[Int] whereas normally they would be able to be passed
  • RecyclingBin[Any], RecyclingBin[Object / AnyRef] may not be passed to RecyclingBin[Paper] whereas normally they would.

So we impose an additional restriction on contravariance with specialized parameters:

  • If A[F1] is to be interpreted as A[F2] under A[-T: Specialized],we require that SpecType(F1) = SpecType(F2). Given that we also require F1 >:> F2, and looking at the definition of SpecType this is roughly equivalent to saying F1 may not be any of the top classes Any, AnyVal, AnyRef unless F2 is also.

If this restriction is not satisfied, warning E233 is emitted, along with a type error.

In order that the creator of the contravariant specialized trait is aware of this restriction, the warning E233 is emitted at the point of definition.

Covariance has a similar problem:

  • In general it works fine because it corresponds to interpreting A[F1] as A[F2] where F1 <:< F2. EitherA[F1] and A[F2] both erase to the same type (A$sp$SpecType(F2) or A if F1 and F2 are both top classes), or A[F1] erases to A$sp$F1 and A[F2] erases to A. But A$sp$F1 is a subtype of A by definition so the upcast will succeed (and upcasts are generally cheap compared to downcasts on the JVM so this is acceptable from a performance perspective).
  • The only exception is with Nothing, because we want to interpret A[Nothing] as e.g. A[Int], but we erase A[Nothing] to A. A >:> A$sp$Int so this doesn't work and we also have to ban it. This makes the code less ergonomic in some cases. For example case object Nil extends List[Nothing] has to become inline trait NilC[T: Specialized] extends List[T] and inline def Nil[T: Specialized] = new NilC[T] () {}, but we don't lose too much expressivity.

Example

inline trait Foo[-T: Specialized]  // warning: Type parameter is both Specialized and contravariant.

Error

-- [E234] Potential Issue Warning: example.scala:1:18 --------------------------
1 |inline trait Foo[-T: Specialized]  // warning: Type parameter is both Specialized and contravariant.
  |                  ^
  |Type parameter is both Specialized and variant. This imposes additional typing restrictions.
  |-----------------------------------------------------------------------------
  | Explanation (enabled by `-explain`)
  |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  | Specialized traits achieve a performance gain through a special erasure.
  |     - Primitives are specialized: Foo[Int] erases to Foo$sp$Int
  |     - Reference types are specialized to the highest non-top class: Foo[Lion] erases to Foo$sp$Animal
  |     - Top classes are erased normally: Foo[Any] / Foo[AnyVal] / Foo[Object] / Foo[AnyRef] erase to Foo.
  |     This means that certain variance patterns that cross these erasure categories will fail at 
  |     runtime due to a ClassCastException, so they are not permitted.
  |     
  |     For example, treating Foo[Any] as Foo[Animal] via contravariance is not allowed with Specialized. 
  |
  |     Please see the docs for more information on how specialized traits are erased.
  |
  |     If you accept this limitation you can silence this warning with @nowarn. For example:
  |     
  |     @nowarn("id=E234")
  |     inline trait Foo[-T: Specialized]:
  |  
  |     Otherwise, remove Specialized, or remove the variance.
  |     
   -----------------------------------------------------------------------------

Solution

Accept downsides and ignore warning as follows, or remove one of Specialized / variant.

import scala.annotation.nowarn
@nowarn("id=E234")
inline trait Foo[-T: Specialized]