Simple examples to illustrate the differences between invariance, covariance and contravariance in Scala.
Consider the following Animal
type and Rescue
and Clinic
type classes:
sealed abstract class Animal(val name: String)
object Animal:
final case class Cat(
override val name: String,
livesRemaining: Int
) extends Animal(name)
final case class Dog(
override val name: String,
breed: Option[DogBreed]
) extends Animal(name)
trait Rescue[+A]:
extension (name: String) def adopt: A
trait Clinic[-A]:
extension (a: A) def examine: String
Now, let's define methods to adopt an animal and take a dog to the vet:
def adopt(name: String)(using rescue: Rescue[Animal]): Animal =
rescue.adopt(name)
def takeToTheVet(dog: Dog)(using clinic: Clinic[Dog]): String =
clinic.examine(dog)
Assuming instances of Rescue[Animal]
and Clinic[Dog]
have been defined, we could invoke the above methods as
follows:
val teddy = adopt("Teddy")
println(s"Welcome home ${teddy.name}!")
val médor = Dog(name = "Médor", breed = Some(DogBreed.Labrador))
takeToTheVet(médor)
Let's assume however, that we do not have Rescue[Animal]
nor Clinic[Dog]
instances. Instead, all we have are
Rescue[Dog]
and Clinic[Animal]
:
val teddy = adopt("Teddy")(using summon[Rescue[Dog]])
println(s"Welcome home ${teddy.name}!")
val médor = Dog(name = "Médor", breed = Some(DogBreed.Labrador))
takeToTheVet(médor)(using summon[Clinic[Animal]])
The code above is able to compile thanks to Rescue
being covariant in A
and Clinic
being contravariant in A
.