When to use covariance and contravariance when designing a generic class
I am learning functional programming and I am trying to understand the concept of covariance and contravariance. My problem is this: I don't know when to apply covariance and contravariance to a generic type. In a specific example, yes, I can determine. But in general, I don't know what the general rule is.
For example, here are some of the rules I've learned:
- If the generic type acts as a parameter: using contravariance. (1)
- If the generic type acts as a return value: use covariance. (2)
In some languages โโthat I knew while this concept also used these conventions. For example: keyword in 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 an exception:
-
methodA(Iterator<A>)
A must be covariance -
methodA(Comparator<A>)
A must be contravariance.
So there is an exception here: although both cases use a common type as input, one must be covariance and the other must be contravariance. My question is: Do we have any general rule for defining covariance / contravariance when designing a class.
thank
source to share
Covariance and contravariance are similar to numbers in arithmetic, and when you set a position with a change in another, the composition is different.
For comparison:
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 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 parameter of the method as a whole is in a contravariant position, and A
is in a covariant position inside Iterator
, but -[+[A]] = -[A]
, therefore, A
in fact , it is in a contravariant position inside this signature and the class should say -A
. This makes sense, the external user is missing a bunch of A
s, so it should be contravariant.
Similarly, in
def method(comp: Comparator[A])
the entire method parameter is in contravariant position and A
is in contravariant position inwardly Comparator
, so you compose them -[-[A]] = +[A]
and you see what is A
really in covariant position. It also makes sense. When you pass A
in Comparator
, the external user has control over what they do, and so it looks a bit like returning A
to them.
source to share