====== Lab 9. Covariance and contravariance ======
In this lab we will be presenting covariance and contravariance for generic types in Scala.
** Objectives: **
* get yourself familiar with covariance and contravariance
* see the benefits of using these concepts
*Introduction*
When we define a generic type, for example ''Shelter[T]'', and we have a types ''Animal'' and ''Dog'' and ''Dog extends Animal'', the type ''Shelter[Dog]'' does not extend type ''Shelter[Animal]''. However, sometimes we would like to have a conversion in one direction or the other. For example, consider the following code:
trait Shelter[+T] {
def sheltered: T
}
In this case we can assign a value of type ''Shelter[Dog]'' to a variable of type ''Shelter[Animal]'' because the trait only outputs ''sheltered'' so when we cast ''Shelter[Dog]'' to ''Shelter[Animal]'' we will expect ''sheltered'' to be ''Animal'' which ''Shelter[Dog]'' provides. ''+T'' means that the generic parameter is covariant, meaning it preserves the cast direction, if ''Dog → Animal'' then ''Shelter[Dog] → Shelter[Animal]''. Remember, if your trait/interface always uses a generic parameter as an output type in the methods it can be made covariant.
Contravariance works in the other direction:
trait Predicate[-T] {
def test(value: T): Boolean
}
We can transform a ''Predicate[Animal]'' into a ''Predicate[Dog]'' because the ''test'' method for ''Predicate[Animal]'' requires us to provide a value of type ''Animal'' which can be restricted to just values of type ''Dog''. So, ''-T'' means that the generic parameter is contravariant, it reverses the cast direction, if ''Dog → Animal'' then ''Predicate[Animal] → Predicate[Dog]''. Remember, if your trait/interface always uses a generic parameter as an input type in the methods it can be made contravariant.
Note that when we don't specify ''+'' or ''-'' the generic parameter is invariant.
We can also do restrictions on the generic parameter like ''T <: Animal'', which means that the generic parameter is can be only a subtype of ''Animal'', something that extends it. The other way around ''T >: Corgi'' means that ''T'' can be a supertype of ''Corgi'', meaning if ''Corgi'' extends ''Dog'', T can be ''Corgi'', ''Dog'' or '' Animal''.
**9.1.** Define the following types:
Start by creating a new Scala project and use the following code:
trait Animal { def name: String }
class Dog(val name: String) extends Animal { def makesSound: String = "Woof!" }
class Cat(val name: String) extends Animal { def makesSound: String = "Mew!" }
trait Shelter[+T] {
def name: String
def residents: List[T]
}
trait Predicate[-T] {
def test(value: T): Boolean
}
**9.2.** Modify this type to use the correct variance to the generic parameter:
case class AnimalShelter[T](name: String, residents: List[T]) extends Shelter[T]
**9.3.** Using these values create a list from them, use the correct type:
val dogShelter = AnimalShelter("Barking Buddies", List(new Dog("Rex"), new Dog("Bo")))
val catShelter = AnimalShelter("Whiskers Haven", List(new Cat("Garfield"), new Cat("Tom")))
val generalShelter = AnimalShelter("City Zoo Rescue", List(new Dog("Rover"), new Cat("Luna")))
val allShelters: List[???] = List(dogShelter, catShelter, generalShelter)
**9.4.** Using these values create a list from them, use the correct type:
val generalNameFilter: Predicate[Animal] = new Predicate[Animal] {
def test(a: Animal): Boolean = a.name.length > 3
}
val dogSoundFilter: Predicate[Dog] = new Predicate[Dog] {
def test(d: Dog): Boolean = d.makesSound == "Woof!"
}
val allFilters: List[???] = List(generalNameFilter, dogSoundFilter)
**9.5.** Considering the code from the previous exercises complete the following code and make it such that for a shelter we get all the animals that match the predicate:
def crossJoinAndFilter[T](shelter: Shelter[T], predicate: Predicate[T]): Unit = {
println(s"Processing '${shelter.name}' using a filter...")
val matches = ???
if (matches.isEmpty) {
println(" -> No animals matched this criteria.")
} else {
println(s" -> There were ${matches.length} matches.")
}
}
Notice the invariance of the generic parameter, it will come up in the last exercise.
**9.6.** Apply the ''crossJoinAndFilter'' method on every possible shelter-predicate combination from the previous exercises, note which ones compile and which none don't.
**9.7.** Complete the following code and use the proper bound and use it on some shelter:
def inspectShelter[???T???](shelter: Shelter[T]): Unit = {
println(s"Inspecting animal Shelter: ${shelter.name}")
//Print all the residents
}
**9.8.** Complete the following code and use the proper bound to implement it, start from a shelter for ''Corgi'' and then expand it by adding new types of residents:
class Corgi(name: String) extends Dog(name)
def saveAnimal[???T???](newResident: T): Shelter[T] = {
// Returns a new Shelter[T] containing the old residents + the new resident
}
**9.9.** Apply the proper generic types to this function and test it, ''S'' is the type of residents in the shelter and ''P'' is the type to test in the predicate, chose the proper bound:
def safeCrossJoin[???](shelter: Shelter[S], predicate: Predicate[P]): Unit = {
println(s"Safely filtering ${shelter.name}:")
shelter.residents.foreach { animal =>
if (predicate.test(animal)) {
println(s" -> Passed check: ${animal.name}")
}
}
}