Edit this page Backlinks This page is read only. You can view the source, but not change it. Ask your administrator if you think this is wrong. ====== 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 ''Box[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: <code scala> trait Shelter[+T] { def sheltered: T } </code> 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: <code scala> trait Predicate[-T] { def test(value: T): Boolean } </code> 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: <code scala> 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 } </code> **9.2.** Modify this type to use the correct variance: <code scala> case class AnimalShelter[T](name: String, residents: List[T]) extends Shelter[T] </code> **9.3.** Using these values create a list from them, use the correct type: <code scala> 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) </code> **9.4.** Using these values create a list from them, use the correct type: <code scala> 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) </code> **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: <code scala> 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 { matches.foreach(animal => println(s" -> MATCH: ${animal.name}")) } } </code> **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: <code scala> def inspectShelter[???T???](shelter: Shelter[T]): Unit = { println(s"Inspecting animal Shelter: ${shelter.name}") //Print all the residents } </code> **9.7.** 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: <code scala> class Corgi(name: String) extends Dog(name) def saveAnimal[???T???](newResident: T): Shelter[T] = { //Create a new shelter by adding the new resident } </code> **9.8.** 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}") } } }