Co-Variance

Co-Variance is not new for Scala and Java developers. This already exists while we overriding a function in subclasses we can define covariance return type in the method signature as below:

class ShoppingCart {
   def bucket(apple : Apple): Fruit = apple
}

class FruitCart extends ShoppingCart {
   override def bucket(apple : Apple): Apple = apple // valid override method
}

In the above code, we are overriding the method bucket and defining return type Apple instead of Fruit, which is called Co-Variance return type. The compiler allows us to define a subclass in the override definition because according to the polymorphism we can assign a subclass object to its supertype reference. Let’s take an example:

val shoppingCart : ShoppingCart = new FruitCart
val fruit : Fruit = shoppingCart.bucket(new Apple) 
/* At runtime this calls to FruitCart bucket which returns Apple and because of 
polymorphism we can assign apple object to Fruit without any problem */

According to above example, it doesn’t matter shoppingCart.bucket(...) return which Fruit, because Fruit is a superclass of all fruits according to the polymophism we can always catch any fruits objects(like Apple, Orange, etc) via Fruit reference variable.

Note: We can use the co-variance return type in Java from version 5 onwards.

The way we are declaring the subclass, in override method signature is called co-variance return type, something similar we are performing in parameterized type co-variance. As shown in the Diagram 10.1, with the help of co-variance we can pass subclass parameterize type to superclass parameterize type. As per our example, we can pass or assign the object of Garage[Lamborghini] (Garage of Lamborghini) or Garage[Jaguar] (Garage of Jaguar) where it accepts Garage[Car] (Garage of Car) method argument or a reference variable.

Again, remember one thing carefully, co-variance defines the relationship between Garage[Lamborghini] (Garage of Lamborghini) with Garage[Car] (Garage of Car) on the basis of passed type paramter. For creating a co-variance type we need to define one special syntax like below:

class Garage[+T](val car : T) {
	def washTheCar : T =  car
}

val lamborginiGarage = new Garage[Lamborghini](new Lamborghini)
val jaguarGarage = new Garage[Jaguar](new Jaguar)

def carGarage(gaurage : Garage[Car]) = gaurage.washTheCar

carGarage(lamborginiGarage) // it compiles and run successfully
res0: Car = Lamborghini@5fb3111a

carGarage(jaguarGarage) //it compiles and run successfully
res1: Car = Jaguar@1c2dd89b

Note: plus symbol (+) is only allow ot use during define the types( like Class, Traits), you cannot use with variables and generics method.

In our example, we successfully created co-variance type Garage[+T] which enables Garage[Car] inheritance hierarchy, where we can pass any subtype of Garage[Car], and those are Garage[Lamborghini] and Garage[Jaguar]. Method carGarage(Garage[Car]) accepts subtype of Garage[Car] objects.

Even, if some methods accept Garage[Lamborghini] or Garage[Jaguar] parameter, you can pass subclass of Garage[Lamborghini] or Garage[Jaguar] like Garage[LamborghiniUrus] or Garage[JaguarXF], but you can’t pass supertype like Garage[Car] to those methods because co-variance only accepts same or sub-hierarchy.

Co-Variance parameters have one important catch or you can say rule which we discuss below: While creating a generic type with the co-variance flag (+), the positions of the type parameter are only valid at methods return type location and variable datatype position as below:

class Garage[+T](val car : T) { // valid position of type parameter T
	def washTheCar : T =  car // valid position of type parameter T
	def repairTheCar(car : T): Car // invalid location of type parameter T
}

Co-Variance FAQ

Why does co-variance allow declaring the type parameter at method return type position, not in method arguments position?

Before moving to the answer to this question, again go back and reminds our variance secrets.

When we creating an object of Garage[Car] just assume something happens under the hood like below code:

class Garage[Car](val car: Car){
	def washTheCar: Car = car
}

When we creating an object of Garage[Lamborghini] just assume something happens under the hood like below code:

class Garage[Lamborghini](val car: Lamborghini){
	def washTheCar: Lamborghini = car
}

something similar with Garage[Jaguar] as well.

We have a method called carGarage(Garage[Car]) which accepts an object of Garage[Car] type as an argument which is co-variance. When we pass the object of Garage[Lamborghini] compiler checks, rule called Liskov Substitution Principle(LSP), where we can replace the object of supertype with subtype. Let's take an example:

class Garage[+T](val car : T) { 
	def washTheCar : T =  car 
	def repairTheCar(car : T): Car // Let’s assume T is valid here.
}

// while creating a Garage[Car] object
class Garage[Car](val car : Car) { 
	def washTheCar : Car =  car 	
    def repairTheCar(car : Car): Car
}

// while creating a Garage[Lamborgini] object
class Garage[Lamborgini](val car : Lamborgini) { 
	def washTheCar : Lamborgini =  car 	
    def reparTheCar(Lamborgini : Lamborgini): Car 
}

def carGarage(garage: Garage[Car], car : Car) = {
	garage.repairTheCar(car)
}

Okay, let’s suppose this code is valid what are the chances we will face runtime exception? There are many, in method carGarage(Garage[Car], Car) If someone pass Garage[Lamborgini] type object and Jaguar type object as parameters, there is the chance we will get a runtime exception because at runtime actual method is repairTheCar(Lamborgini) and we are passing Jaguar object, the first problem is ClassCaseException, Second problem, suppose some internal conversion happens it converts Jaguar to Lamborgini but we lose the value of Lamborgini features and there is a possibility we are using some those Lamborgini particular features within the method repairTheCar(Lamborgini) which are not available in converted Lamborgini type which generates runtime exception:

val lamborginiGarage = new Garage[Lamborghini](new Lamborghini)
carGarage(lamborginiGarage, new Jaguar) // something bad during runtime

This whole scenario violates the LSP where we do not completely replace subtype in the place of supertype in case of repairTheCar(car: T) method call so that’s why the compiler does not allow us to define the type parameter at method argument level.

Why does co-variance allow type parameter at the location of variable datatype?

There is a simple answer because in Scala variables and methods are handled by the same namespace.

Is there any way to using the type parameter at the method argument location?

Yes, there is a way, but this is not as aspected. In chapter 9th we have an idea about type constraints where we are using Least Upper Bound and Greatest Lower Bound. For using type argument in co-varaince as a method parameter than Greatest Lower Bound comes into the picture. Let's take an example:

class Garage[+T] {
    def repairTheCar[U >: T](car: U): Any = car
}

In method repairTheCar(car: U) we defined Lower Bound means, the U type is greater than or equals to type T means U could be Any type but not lower than passed T type parameter. Under the hood some thing happen as below:

class Garage[Lamborghini] {
	def repairTheCar[Any >: Lamborghini](car: Any): Any = car // Suppose something similar happens internaly
}

When we create an object of Garage[Lamborghini] type and passed to our method carGarage(Garage[Car], Car), everything is working fine.

def carGarage(garage: Garage[Car], car : Car) = {
	garage.repairTheCar(car)
}

carGarage(lamborginiGarage, new Lamborghini) // Compiles and run successfully
res2: Any = Lamborghini@2941631f

carGarage(lamborginiGarage, new Jaguar) // compiles and run successfully
res3: Any = Jaguar@e2498a3

The carGarage(lamborginiGarage, new Jaguar) method call is compiled and run successfully because during the compilation, compilers know, repairTheCar(Any) method accepts Any type parameter if someone can pass any subtype of like Jaguar with Garage[Lamborghini], there is no issue because repairTheCar(Any) can only use Any type features no there specific one, which are available in both Lamborghini and Jaguar objects.

Why does compiler not allow to pass supertype object in co-variance like `Garage[FourWheeler]` and generates compile time error?

For answering the question let’s take an example:

class FourWheeler
class Car extends FourWheeler
class Lamborghini extends Car
class Jaguar extends Car

class Garage[+T](val car : T) {
    def washTheCar: T = car
}

def carGarage(garage: Garage[Car]): Car = {
	garage.washTheCar
}

val lamborginiGarage = new Garage[Lamborghini](new Lamborghini)
carGarage(lamborginiGarage, new Lamborghini) // Compiles successfully

val carGarage = new Garage[Car](new Car)
carGarage(carGarage, new Car) // Compiles successfully

val fourWheelerGarage = new Garage[FourWheeler](new FourWheeler)
carGarage(fourWheelerGarage, new FourWheeler) // Compile time error

While we are passing Garage[FourWheeler] to carGarage(Garage[Car], Car) method, the compiler gives us an error because the compiler knows, Garage[Car] maybe contains some method, which returns Car type object, but in Garage[FourWheeler] the same method must return FourWheeler type object, as per hierarchy FourWheeler is a superclass of Car and as per the polymorphism it is not valid to catch the superclass object to subclass. Let's elaborate this via code:

// while creating Garage[Lamborghini]
class Garage[Lamborghini](val car : Lamborghini) {
    def washTheCar: Lamborghini = car
}

// while creating Garage[Car]
class Garage[Car](val car : Car) {
    def washTheCar: Car = car
}

// while creating Garage[FourWheeler]
class Garage[FourWheeler](val car : FourWheeler) {
    def washTheCar: FourWheeler = car
}

val car: Car = carGarage(lamborginiGarage, new Lamborghini)
// returns Lamborghini which is valid

val car: Car = carGarage(carGarage, new Car)
// returns Lamborghini which is valid

val car: Car = carGarage(fourWheelerGarage, new FourWheeler) // suppose this compiles

While carGarage method call washTheCar(car) method as per implementation, it returns FourWheeler type object, which is not possible to assing to the sub-class reference Car type that's why in co-varaince super types are not allow.

Last updated