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 s
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
.
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:
Now a more detailed description with compilable Scala code.
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]
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
A similar discussion about ? extends A
and ? super B
in Java 8:
Java 8 Comparator comparing() static function
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]
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.