Scala contravariance and covariance

2020-06-06 17:23发布

问题:

I am playing around with the typesystem of scala and found a strange case. I have a strong reason to believe, I don't understand covariant and covariance.

This is my problem case:

I have two classes, Point and ColorPoint, which is a subclass of Point.

class Point(val x : Int, val y : Int)
class ColorPoint(x : Int, y : Int, val red : Int, val green : Int, val blue : Int) extends Point(x,y) 

This class casts B to A, while B should be a supertype of A:

class CoVariance[+A]{
 def cast[B >: A](x : B) : A = {
  return x.asInstanceOf[A] 
 }
}

This class casts B to A, while B should be a supertype of A:

class ContraVariance[-A]{
 def cast[B, A <: B](x : B) : A = {
    return x.asInstanceOf[A]
 }
}

Case 1:

val co = new CoVariance[Point]
val color_point = new ColorPoint(1,2,3,4,5)
val point_co = co.cast(color_point) 
println(point_co.x)

If I write this out:

// Covariance[Point] -> 
// cast[B :> Point](x : B) : Point -> (fill in ColorPoint)
// Cast[ColorPoint :> Point] : Point 

I would expect this to be incorrect, because ColorPoint is not a supertype of Point, but scala doesn't complain.

Next one:

val contra = new ContraVariance[Point]
val color_point_contra = new ColorPoint(1,2,3,4,5)
val point_contra = contra.cast(color_point_contra) 
println(point_contra.x)

If I write this out:

// ContraVariance[Point] -> 
// cast[B, Point <: B](x : B) : Point -> (fill in ColorPoint)
// cast[ColorPoint, Point <: ColorPoint] : Point 

I also expect this to be incorrect, but scala doesn't complain. I would say Point is not a subtype of ColorPoint.

Is my reasoning correct or am I missing something?

回答1:

I think you're misunderstanding what covariant and contravariant positions are. It doesn't imply that you're able to cast between certain types, it just establishes the inheritance relationship between parameterized types.

You're only able to mark type parameters as in covariant or contravariant positions. When you say Container[+A], you're saying that you can treat all instances of Container[A] as subtypes of Container[B] if A is a subtype of B. This makes sense for immutable container classes: You can think of a List[Person] to be a parent of List[Employee]. Note this says nothing about casting rules -- those go unchanged.

Contravariant is similar, but the opposite. If you have Writer[-A], it says Writer[A] is a subtype of Writer[B] if B is a subtype of A. You can see how this makes intuitive sense too: If you have a Writer[Person] as something that can write a Person into some destination and you have Writer[Employee] as a writer that can only write Employees, it makes sense for Writer[Employee] to be a parent of Writer[Person] since writing a Person is a sub-task of writing a full Employee, even though it is the opposite for the types themselves.



回答2:

  1. asInstanceOf[T] ignores typechecks. So you may even have the following cast:

    def cast[B](a:A):B = a.asInstanceOf[B]
    

    for any A and B.

    Thus in your case Scala won't complain.

  2. If I understand correctly, you want to have cast method only when the types are in proper relations (parent-child). I think, you needn't have +/- in class declaration. Only two different casts:

    implicit class CastToParent[A](a:A) {
      def cast[B >: A]:B = a.asInstanceOf[B]
    }
    
    
    implicit class CastToChild[A](a:A) {
      def cast[B <: A]:B = a.asInstanceOf[B]
    }
    

    this allows you to have the desired conversions.

    trait A
    trait B extends A
    trait C
    val a:A = new B {}
    val b   = a.cast[B] //parent to child
    val a1  = b.cast[A] //child to parent.
    val c   = a.cast[C] // don't compile