I was going through the below code here
class Pets[+A](val pet:A) {
def add(pet2: A): String = "done"
}
And on compiling it we get the error
<console>:9: error: covariant type A occurs in contravariant position in type A of value pet2
The reason given for it is : -
The compiler prevents us from falling into the absurdity of calling pets.add (Dog ()), since it is a set of Cat.
Isn’t the compiler assuming here that I am maintaining a Set
(or a collection) of Cat
internally? I might intend to do something totally different which is type safe. But the compiler prevents me from doing this.
Could you give an example?
Perhaps it would help to expand on the error message a bit. Assume the compiler allowed this. If it did, then Pets[Cat]
would be a subtype of Pets[Animal]
. So if we had a method addPet(pets: Pets[Animal])
then we would be able to pass it an instance of Pets[Cat]
. However, it is perfectly find to call pets.add(Dog())
on a Pets[Animal]
, so if this compiled, you could do something that shouldn’t be allowed. Perhaps the following code shows this even more clearly.
val cats = new Pets[Cat]
val pets: Pets[Animal] = cats // works if Pets[Cat] <: Pets[Animal]
pets.add(Dog())
If Pets
could be covariant in A
then this would be perfectly valid code, even though it clearly shouldn’t be allowed. The “position” of A
as an input to a method makes covariance unsafe. Return positions are fine, but being in an input causes problems. The inverse is true for contravariance.
This is why the signature of methods like ::
which “add to” Lists look like the following.
def::[B >: A](elem: B): List[B]
Note that the input isn’t of type A
but of type B
which must be a supertype of A
. You might be able to mirror this type of thing in your Pets
class, depending on what you are doing with it.
1 Like
Following up on MarkCLewis’s post since I found this confusing when I first learned about it. Changing your code to
class Pets[+A](val pet: A) {
def add[B >: A](pet2: B): String = "done"
}
will avoid the compiler complaining. What this does is tells tells the compiler to only allow types that A is a supertype of. The variance of Pets says you can also use anything that is a subtype of A. So the compiler works out that ONLY things of type A are allowed (A being the only subtype and supertype of A). It’s basically a technique to make A invariant for def add
.
There are two reasons. First, it is too easy for the result to do something entirely different that what seems obvious. It’s why the default polymorphism is strict. One really needs to deeply understand the schematics to predict the outcome of large systems. Second is the halting problem. If one uses both +T and -U, the type calculus is not to halt. It’s rare, but it can happen. Hopefully the compiler will crash rather than the runtime system hang. Positional schematics greatly reduces both problems. At the price of being annoying.