How to check covariant and contravariant position of an element in the function?
Asked Answered
P

2

12

This is a code snippet from one of the articles that I read regarding contravariance and covariance in scala. However, I fail to understand the error message thrown by the scala compiler "error: covariant type A occurs in contravariant position in type A of value pet2

class Pets[+A](val pet:A) {
  def add(pet2: A): String = "done"
}

My understanding of this code snippet is that Pets is covariant and accepts objects that are subtypes of A. However, the function add takes in a parameter of type A only.Being covariant means that the Pets can take parameters of Type A and its subtypes. Then how is this supposed to throw error. From where is the question of contravariance even arising.

Any explanation to the above error message will be highly helpful. Thanks

Pawnbroker answered 15/2, 2018 at 16:43 Comment(0)
U
45

TL;DR:

  • Your Pets class can produce values of type A by returning the member variable pet, so a Pet[VeryGeneral] cannot be a subtype of Pet[VerySpecial], because when it produces something VeryGeneral, it cannot guarantee that it is also an instance of VerySpecial. Therefore, it cannot be contravariant.

  • Your Pets class can consume values of type A by passing them as arguments to add. Therefore a Pet[VerySpecial] cannot be a subtype of pet Pet[VeryGeneral], because it will choke on any input that is not VerySpecial. Therefore, your class cannot be covariant.

The only remaining possibility is: Pets must be invariant in A.


###An illustration: Covariance vs. Contravariance:

I'll use this opportunity to present an improved and significantly more rigorous version of this comic. It is an illustration of the covariance and contravariance concepts for programming languages with subtyping and declaration-site variance annotations (apparently, even Java people found it sufficiently enlightening, despite the fact that the question was about use-site variance).

First, the illustration:

covariance-contravariance-comic

Now a more detailed description with compilable Scala code.

###Explanation for Contravariance (left part of Figure 1)

Consider the following hierarchy of energy sources, from very general, to very specific:

class EnergySource
class Vegetables extends EnergySource
class Bamboo extends Vegetables

Now consider a trait Consumer[-A] that has a single consume(a: A)-method:

trait Consumer[-A] {
  def consume(a: A): Unit
}

Let's implement a few examples of this trait:

object Fire extends Consumer[EnergySource] {
  def consume(a: EnergySource): Unit = a match {
    case b: Bamboo => println("That's bamboo! Burn, bamboo!")
    case v: Vegetables => println("Water evaporates, vegetable burns.")
    case c: EnergySource => println("A generic energy source. It burns.")
  }
}

object GeneralistHerbivore extends Consumer[Vegetables] {
  def consume(a: Vegetables): Unit = a match {
    case b: Bamboo => println("Fresh bamboo shoots, delicious!")
    case v: Vegetables => println("Some vegetables, nice.")
  }
}

object Panda extends Consumer[Bamboo] {
  def consume(b: Bamboo): Unit = println("Bamboo! I eat nothing else!")
}

Now, why does Consumer have to be contravariant in A? Let's try to instantiate a few different energy sources, and then feed them to various consumers:

val oilBarrel = new EnergySource
val mixedVegetables = new Vegetables
val bamboo = new Bamboo

Fire.consume(bamboo)                // ok
Fire.consume(mixedVegetables)       // ok
Fire.consume(oilBarrel)             // ok

GeneralistHerbivore.consume(bamboo)           // ok
GeneralistHerbivore.consume(mixedVegetables)  // ok
// GeneralistHerbivore.consume(oilBarrel)     // No! Won't compile

Panda.consume(bamboo)               // ok
// Panda.consume(mixedVegetables)   // No! Might contain sth Panda is allergic to
// Panda.consume(oilBarrel)         // No! Pandas obviously cannot eat crude oil

The outcome is: Fire can consume everything a GeneralistHerbivore can consume, and in turn GeneralistHerbivore can consume everything a Panda can eat. Therefore, as long as we care only about the ability to consume energy sources, Consumer[EnergySource] can be substituted where a Consumer[Vegetables] is required, and Consumer[Vegetables] can be substituted where a Consumer[Bamboo] is required. Therefore, it makes sense that Consumer[EnergySource] <: Consumer[Vegetables] and Consumer[Vegetables] <: Consumer[Bamboo], even though the relationship between the type parameters is exactly the opposite:

type >:>[B, A] = A <:< B

implicitly:          EnergySource  >:>          Vegetables
implicitly:          EnergySource                           >:>          Bamboo
implicitly:                                     Vegetables  >:>          Bamboo

implicitly: Consumer[EnergySource] <:< Consumer[Vegetables]
implicitly: Consumer[EnergySource]                          <:< Consumer[Bamboo]
implicitly:                            Consumer[Vegetables] <:< Consumer[Bamboo]

###Explanation for Covariance (right part of Figure 1)

Define a hierarchy of products:

class Entertainment
class Music extends Entertainment
class Metal extends Music // yes, it does, seriously^^

Define a trait that can produce values of type A:

trait Producer[+A] {
  def get: A
}

Define various "sources"/"producers" of varying levels of specialization:

object BrowseYoutube extends Producer[Entertainment] {
  def get: Entertainment = List(
    new Entertainment { override def toString = "Lolcats" },
    new Entertainment { override def toString = "Juggling Clowns" },
    new Music { override def toString = "Rick Astley" }
  )((System.currentTimeMillis % 3).toInt)
}

object RandomMusician extends Producer[Music] {
  def get: Music = List(
    new Music { override def toString = "...plays Mozart's Piano Sonata no. 11" },
    new Music { override def toString = "...plays BBF3 piano cover" }
  )((System.currentTimeMillis % 2).toInt)
}

object MetalBandMember extends Producer[Metal] {
  def get = new Metal { override def toString = "I" }
}

The BrowseYoutube is the most generic source of Entertainment: it could give you basically any kind of entertainment: cat videos, juggling clowns, or (accidentally) some music. This generic source of Entertainment is represented by the archetypical jester in the Figure 1.

The RandomMusician is already somewhat more specialized, at least we know that this object produces music (even though there is no restriction to any particular genre).

Finally, MetalBandMember is extremely specialized: the get method is guaranteed to return only the very specific kind of Metal music.

Let's try to obtain various kinds of Entertainment from those three objects:

val entertainment1: Entertainment = BrowseYoutube.get   // ok
val entertainment2: Entertainment = RandomMusician.get  // ok
val entertainment3: Entertainment = MetalBandMember.get // ok

// val music1: Music = BrowseYoutube.get // No: could be cat videos!
val music2: Music = RandomMusician.get   // ok
val music3: Music = MetalBandMember.get  // ok

// val metal1: Metal = BrowseYoutube.get   // No, probably not even music
// val metal2: Metal = RandomMusician.get  // No, could be Mozart, could be Rick Astley
val metal3: Metal = MetalBandMember.get    // ok, because we get it from the specialist

We see that all three Producer[Entertainment], Producer[Music] and Producer[Metal] can produce some kind of Entertainment. We see that only Producer[Music] and Producer[Metal] are guaranteed to produce Music. Finally, we see that only the extremely specialized Producer[Metal] is guaranteed to produce Metal and nothing else. Therefore, Producer[Music] and Producer[Metal] can be substituted for a Producer[Entertainment]. A Producer[Metal] can be substituted for a Producer[Music]. In general, a producer of a more specific kind of product can be subsituted for a less specialized producer:

implicitly:          Metal  <:<          Music
implicitly:          Metal                      <:<          Entertainment
implicitly:                              Music  <:<          Entertainment

implicitly: Producer[Metal] <:< Producer[Music]
implicitly: Producer[Metal]                     <:< Producer[Entertainment]
implicitly:                     Producer[Music] <:< Producer[Entertainment]

The subtyping relationship between the products is the same as the subtyping relationship between the producers of the products. This is what covariance means.


Related links

  1. A similar discussion about ? extends A and ? super B in Java 8: Java 8 Comparator comparing() static function

  2. Classical "what are the right type parameters for flatMap in my own Either implementation" question: Type L appears in contravariant position in Either[L, R]

Ultramicrometer answered 19/2, 2018 at 1:5 Comment(1)
After reading your example, yes this is very clear and lucid as there are thousands of blogs like this apple banana examples in the web. Only thing I don't understand is: \n Why did you chose contravariant example as trait Consumer[-A] { def consume(a: A): Unit } and Covariant example as: trait Producer[+A] { def get: A }Gobioid
C
4

Class Pets is covariant in its type A (because it's marked as +A), but you are using it in a contravariant position. This is because, if you take a look at the Function trait in Scala, you will see that the input parameter type is contravariant, while return type is covariant. Every function is contravariant in its input type and covariant in its return type.

For example, function taking one argument has this definition:

trait Function1[-T1, +R]

The thing is, for a function S to be a subtype of function F, it needs to "require (same or) less and provide (same or) more". This is also known as the Liskov substitution principle. In practice, this means that Function trait needs to be contravariant in its input and covariant in its output. By being contravariant in its input, it requires "same or less", because it accepts either T1 or any of its supertypes (here "less" means "supertype" because we are loosening the restriction, e.g. from Fruit to Food). Also, by being covariant in its return type, it requires "same or more", meaning that it can return R or anything more specific than that (here "more" means "subtype" because we are adding more information, e.g. from Fruit to Apple).

But why? Why not the other way around? Here's an example that will hopefully explain it more intuitively - imagine two concrete functions, one being subtype of another:

val f: Fruit => Fruit
val s: Food => Apple

Function s is a valid subtype for function f, because it requires less (we "lose" information going from Fruit to Food) and provides more (we "gain" information going from Fruit to Apple). Note how s has an input type that's a supertype of f's input type (contravariance), and it has a return type that's a subtype of f's return type (covariance). Now let's imagine a piece of code that uses such functions:

def someMethod(fun: Fruit => Fruit) = // some implementation

Both someMethod(f) and someMethod(s) are valid invocations. Method someMethod uses fun internally to apply fruit to it, and to receive fruit from it. Since s is a subtype of f, that means we can supply Food => Apple to serve as a perfectly good instance of fun. Code inside someMethod will at some point feed fun with some fruit, which is OK, because fun takes food, and fruit is food. On the other hand, fun having Apple as return type is also fine, because fun should return fruit, and by returning apples it complies with that contract.

I hope I managed to clarify it a bit, feel free to ask further questions.

Cherilyncherilynn answered 15/2, 2018 at 17:9 Comment(2)
How should this function "def add(pet2: A): String" be modified so as to remove the compilation errorPawnbroker
@ChaitanyaWaikar Depends on what you're trying to do... I would always rather move away from variance and define class Pets[A], but if that's not an option, you can circumvent it by def add[B >: A](pet2: B): String = "done".Cherilyncherilynn

© 2022 - 2024 — McMap. All rights reserved.