2

My understanding is that the type variance is used in the following cases:

  • If a generic type G has type parameter T1, which appears as a type of an argument of a G method, then G can be contravariant in T1.

  • If G has type parameter T2, which appears as a type of any return value of a method (or ctor) of G, then G can be covariant in T2.

What if I replace can be with should be in the sentences above? Are there any other case of co- and contra-variant usage? When and why do you make your types co- and contra-variant?

Jacek Laskowski
  • 72,696
  • 27
  • 242
  • 420
Michael
  • 10,185
  • 12
  • 59
  • 110

3 Answers3

3

Quoting from the spec, section 4.5 Variance Annotations:

Variance annotations indicate how instances of parameterized types vary with respect to subtyping (§3.5.2). A '+' variance indicates a covariant dependency, a '-' variance indicates a contravariant dependency, and a missing variance indication indicates an invariant dependency. A variance annotation constrains the way the annotated type variable may appear in the type or class which binds the type parameter. In a type definition type T [tps] = S, or a type declaration type T [tps] >: L <: U type parameters labeled +' must only appear in covariant position whereas type parameters labeled '-' must only appear in contravariant position.

A type parameter is therefore by default considered to be invariant. You have to explicitly annotate the type parameter to be either co- or contravariant if you want to use this. Also, it is perfectly legal to use variance annotations on a type parameter that is not used at all (although his may not be so useful). For example:


scala> class A[+T, -S] {def myMethod(s: String) = println(s)}
defined class A

scala> class A2[T] {def myMethod(t: T) = println(t)}
defined class A2

scala> class A3[-T] {def myMethod(t: T) = println(t)}
defined class A3

scala> val a1 = new A2[Any]
a1: A2[Any] = A2@1cd1cea

scala> val a2: A2[Int] = a1
:6: error: type mismatch;
 found   : A2[Any]
 required: A2[Int]
       val a2: A2[Int] = new A2[Any]

scala> val a3  = new A3[Any]
a3: A3[Any] = A3@875dee

scala> val a4: A3[Int] = a3
a5: A3[Int] = A3@875dee

The variance annotation on class A3, which is contravariant in this example, makes that A3[Any] is considered to be a subtype of A3[Int], making the assignment from instance a4 to a3 legal. This fails if you do not use the variance annotation.

Arjan Blokzijl
  • 6,878
  • 2
  • 31
  • 27
  • Thank you for the answer. I am afraid I did not make clear that I am asking about use cases and best practices. I have edited the question now. – Michael Mar 11 '11 at 13:31
  • Ok, that makes sense, and now my answer doesn't make that much sense anymore, but I'll leave it anyway for others to improve upon. – Arjan Blokzijl Mar 11 '11 at 17:36
3

Things are just not that simple. Sometimes variance doesn't make any sense at all, so you just keep the class invariant.

Also, note that variance toggles along a usage chain. For example:

class A[+T]
class B[-T] {
  def f(x: A[T]) {}
}

class C[+T] {
  def g(x: B[T]) {}
}

Or, put in another way, it is not a simple thing that can be described in a few lines. Which is the main reason why the strong enforcement of variance by Scala is a very useful thing -- nowadays, I'm half convinced most code using variance in Java must have subtle bugs.

Daniel C. Sobral
  • 295,120
  • 86
  • 501
  • 681
  • Thank you for the answer. Could you please give an example (use case) when contravariance is useful. For example, I understand that Funtion[+X,-Y] is contravariant in the argument type. My question is _when_ we can use it and _why_ it is useful. – Michael Mar 12 '11 at 12:44
  • @Misha See http://stackoverflow.com/questions/5279539/covariance-and-contravariance-considerations-when-designing/5280098#5280098. – Daniel C. Sobral Mar 12 '11 at 13:01
  • **Fix:** Function[-X,+Y], i.e. function is contravariant in its argument type and covariant in its result type. – Michael Mar 12 '11 at 13:07
  • @Daniel I have read your [answer](http://stackoverflow.com/questions/5279539/covariance-and-contravariance-considerations-when-designing/5280098#5280098) (and upvoted it:)). Unfortunately, it does not provide examples, _why_ and _where_ we should use the contravraiance of Function or scalaz Order trait. – Michael Mar 12 '11 at 13:10
  • @Misha Ah, I see. Well, say a method asks for an `Order[List[Int]]`. Instead of having to come up with a `List`-specific `Order`, you can pass an `Order[Seq[Int]]` to it, which will work because `Order` is contra-variant. – Daniel C. Sobral Mar 12 '11 at 13:16
  • @Daniel I see your point with the `Order` example above. However it does not look like a good design for me. One should define a method with `Order[Seq[Int]]` in the first place. The fact that a method with `Order[List[Int]]` is defined instead looks like a design flaw. – Michael Mar 12 '11 at 13:21
  • @Misha Not necessarily. Consider this: `def sort[CC <: Seq[_] : Order](coll: CC): CC`. If you call it with `sort(List(2,1,3))`, you get `List(1, 2, 3)` back. If you call it with `sort(Vector(2, 1, 3))`, you get `Vector(1, 2, 3)` back. Though you could fix the implicit to be of type `Order[Seq[T]]`, the method would not work, then, if all you had was a `Order[List[T]]`. – Daniel C. Sobral Mar 12 '11 at 14:22
  • @Misha The `[CC : Order]` is just like `[CC]` with an added `(implicit $ev: Order[CC])` parameter automatically added. So this says `CC` is some kind of `Seq`, and that there's an `Order[CC]` for it, which would then be used to perform the sorting. Actually, this method definition isn't quite right, as the idea was sorting sequences, but it only receives one sequence. :-) So make that `(coll: CC*): Seq[CC]`. These are details, though. The important point is that the interface is parameterized, and contra-variance enables it to work with any valid `Order` available. – Daniel C. Sobral Mar 12 '11 at 19:31
1

Let me give a try at this old question. One of the usage of the covariance and contravariance is to have some restraint on the Generic by means of lower bound >: (covariance) and upper bound <: (contravariance). The usage can be seen in the following code snippet. It is from my own blog on the subject.

    abstract class Animal (animalType:String)

    class HasFourLegs(animalType:String) extends Animal(animalType){
      def move=println(this+" walking on four legs")
    }

    class HasTwoLegs(animalType:String) extends Animal(animalType){
      def move=println(this+" walking on Two legs")
    }

    case class Dog(animalType:String) extends HasFourLegs(animalType)
    case class Ostrich(animalType:String) extends HasTwoLegs(animalType)

      def moveOn4legs[T<:HasFourLegs](animal:T)=  animal.move
      val dog = Dog("dog")
      val ostrich=Ostrich("ostrich")
      moveOn4legs(dog)
      /*
      moveOn4legs(ostrich)
      error: inferred type arguments [this.Ostrich] do not conform to method moveOn4legs's type parameter bounds [T <: this.HasFourLegs]
      moveOn4legs(ostrich)
      ^
      */
      println

    class AnimalMovement [+T]{
      def movement[U>:T](animal:U)=println(animal+" walking on Two legs!!!")
    }

      val moveLikeTwoLegs=new AnimalMovement[HasTwoLegs]()
      moveLikeTwoLegs.movement(ostrich)
      moveLikeTwoLegs.movement(dog)
Win Myo Htet
  • 5,377
  • 3
  • 38
  • 56