Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
538 views
in Technique[技术] by (71.8m points)

scala - How to check covariant and contravariant position of an element in the function?

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

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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: Entertainment = BrowseYoutube.get   // No, probably not even music
// val metal2: Entertainment = RandomMusician.get  // No, could be Mozart, could be Rick Astley
val metal3: Entertainment = 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]


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...