3

I'm learning functional programming and I'm trying to understand covariance and contravariance concept. My problem now is: I don't really know when should apply covariance and contravariance to a generic type. In specific example, yes, I can determine. But in general case, I don't know which general rule.

For example here is some rules that I have studied:

  • if generic type acts as parameter: using contravariance. (1)
  • if generic type acts as retrun value: using covariance. (2)

In some languages that I known while stuyding this concept also use those convention. For example: in keyword for covariance(In Scala is +) and out keyword for contravariance (in Scala is -). Point (1) is easy to understand. But at point (2), I see exception:

  • methodA(Iterator<A>) A should be covariance
  • methodA(Comparator<A>) A should be contravariance.

So the exception here is: although both 2 cases using generic type as input but one should be covariance and other should be contravariance. My question is: do we have any general rule to decide covariance/contravariance when designing a class.

Thanks

Trần Kim Dự
  • 5,872
  • 12
  • 55
  • 107

1 Answers1

2

Covariance and contravariance are like the signs of numbers in arithmetic, and when you nest a position with variance in a another, the composition is different.

Compare:

1] +(+a) = +a
2] -(+a) = -a
3] +(-a) = -a
4] -(-a) = +a

and

trait +[+A] { def make(): A } // Produces an A
trait -[-A] { def break(a: A) } // Consumes an A

1]
  // Produces an A after one indirection: x.makeMake().make()
  trait ++[+A] { def makeMake(): +[A] }
  +[+[A]] = +[A]
2]
  // Consumes an A through one indirection: x.breakMake(new +[A] { override def make() = a })
  trait -+[-A] { def breakMake(m: +[A]) }
  -[+[A]] = -[A]
3]
  // Consumes an A after one indirection: x.makeBreak().break(a)
  trait +-[-A] { def makeBreak(): -[A] }
  +[-[A]] = -[A]
4]
  // Produces an A through one indirection
  // Slightly harder to see than the others
  // x.breakBreak(new -[A] { override def break(a: A) = {
  //   you have access to an A here, so it's like it produced an A for you
  // }})
  trait --[+A] { def breakBreak(b: -[A]) }
  -[-[A]] = +[A]

So when you have

def method(iter: Iterator[A])

the method parameter as a whole is in a contravariant position, and the A is in a covariant position inside the Iterator, but -[+[A]] = -[A], so A is actually in a contravariant position inside this signature and the class needs to say -A. This makes sense, the outside user is passing in a bunch of As, so it ought to contravariant.

Similarly, in

def method(comp: Comparator[A])

the whole method parameter is in a contravariant position, and A is in a contravariant position inside the Comparator, so you compose them -[-[A]] = +[A] and you see that A is really in a covariant position. This also makes sense. When you pass an A into the Comparator, the outside user has control over what it does, and so it's a little like returning that A to them.

HTNW
  • 27,182
  • 1
  • 32
  • 60
  • https://stackoverflow.com/questions/2501023/demonstrate-covariance-and-contravariance-in-java as second answer tell us opposite from you. Is there anything differences between your answer and second answer here? – Trần Kim Dự Jul 03 '17 at 03:51
  • More over, as @Dima said, I think if one type can be contravariance and covariance, this type should be invariance right ? – Trần Kim Dự Jul 03 '17 at 03:52
  • The problem is in scoping. In that answer, *inside* `Iterable`, `T` should be covariant, but *outside*, on the *class* level, `T` must therefore be contravariant. Your question is about creating classes, so I used that scope in my answer. That question is about Java, which can't do variance, so they are forced to use wildcards to simulate it on a parameter level. – HTNW Jul 03 '17 at 04:05
  • thanks for your rule. it's really helpful with me. The only thing I don't understand now is its insight. For example, your sentence "When you pass an A into the Comparator, the outside user has control over what it does, and so it's a little like returning that A to them". I don't understand this. Because if outside user has control over what it does, it should be contravariance (I think). Can you give me a more concrete example so I can follow easier. Thanks so much. – Trần Kim Dự Jul 03 '17 at 09:28
  • `trait Collector[-A, +B] { def consume(a: A): Unit; def collect(): B }; class CollectorFeeder[+A](input: Traversable[A]) /* ctor params do not matter for variance */ { def feed(c: Collector[A, _]): Unit = input.foreach(c.consume) }` Now say some user uses this library and does `val feeder = new CollectorFeeder[String](???); feeder.feed(new Consumer[String, Int] { ... })`. As `feed` calls `consume` on each element of `input`, the user is given that `A` and can do whatever with it. Since the user has gotten an `A` *out* of `CollectorFeeder`, it should be covariant in `A`. – HTNW Jul 03 '17 at 15:53
  • Can you explain for me last of your paragraph, who is represent for "the user". feeder or input object. thanks. – Trần Kim Dự Jul 03 '17 at 16:50
  • You are designing some class with a generic type. Someone, probably future-you, will later use that class and instantiate it with type arguments. That someone is the user, who is writing code that uses your type from the outside. On the outside, when you can get an `A` out of `T[A]`, and you can't put an `A` back, then `T` should covariant in `A`. If you pass in a consumer of `A`s (`-[A]`) into a consumer of `-[A]`s (`-[-[A]]`), then when that outer consumer passes an `A` into the `-[A]` it's consuming, that `A` is exposed to the outside (-> covariance). The details aren't important. – HTNW Jul 03 '17 at 17:23