Variance
Table of Contents
1. Covariance
We say some type F
which takes a type paramater is covariant if \( S \subset T \Rightarrow F[S] \subset F[T] \). ie we might reasonably claim a List[Cat]
is a subtype of List[Animal]
. However this may cause some problems:
val myCats: List[Cat] = List(Cat("Max"), Cat("Felix")) val myAnimals: List[Animal] = myCats val myDog: Dog = Dog("Fiedo") myAnimals.add(myDog) // But now myCats contains a dog!!
However, we can overcome this problem by making List
immutable, so that we cannot add any new elements to a list.
2. Contravariance
Sometimes it may also make sense for a particular type \( G \) that if \( S \) is a subtype of \( T \), then \( G[T] \) is a subtype of \( G[S] \) - and we call this contravariance. An example of this could be a JSON encoder. For example say if we have:
trait Animal { def name: String } case class Dog(name: String, age: Int) extends Animal
Then it would make sense that if we had a json encoder for the Animal
type, we could use this to encode Dog
instances also, albeit without the dog's age
:
val animalEncoder: Encoder[Animal] = getEncoder() val dog: Dog = Dog("Fiedo", 3) val dogEncoder: Encoder[Dog] = animalEncoder animalEncoder.encode(dog) // {"name": "Fiedo"}
Hence an Animal
encoder is just one particular type of Dog
encoder, hence the Encoder
type is contravariant. Note that the opposite would hold for a JSON decoder (covariant), and in general data sources can be covariant and data sinks can be contravariant.
3. Gotchas
Suppose we have some covariant type:
class MyList[+T] { def contains(e: ???): Boolean }
Then what should be the type of e
? We might reason that we should have e: T
to match our type parameter, but that gives rise to the following:
val myDogs: List[Dog] = List(Dog("Fiedo"), Dog("Zeus")) val myAnimals: List[Animal] = myDogs // Completely fine by our type signature myAnimals.contains(Cat("Max")) // Not allowed by our type signature! myDogs.contains(Cat("Max"))
So the way we get around this is to make the type of e: U
with the type constraint U :> T
so that the method can take any supertype of T
(also see here for a great SO link). This can however lead to some unexpected behaviour:
// All of the following is valid in scala val myInts = List(1, 2, 3) myInts.contains("not an int") // Boolean = false "not an int" :: myInts // List[Any] = List("not an int", 1, 2, 3)