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

+3


source to share


1 answer


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.

+2


source







All Articles