Contra-Varaince

For most of the Scala developers, contra-variance is most of the trickiest concepts in the Scala type system, but as per my experience if you have a clear picture of co-variance like how it works, why compiler allows the parameter position, why not and most important things are our varaince secrets than contra-variance is simple and easy to understand. Before proceeding to contra-variance, if you have any doubt within co-variance please go once again for clear the picture.

In contra-variance, the inheritance hierarchy arrows are going to be flip. As you can see in our Diagram 10.1, where Garage[Car] is actually a subtype of Garage[Lamborghini], but always remember those flips are only for parameterized types object like Garage[Car](Garage of Car) not with actual type hierarchy like Car, Lamborghini and more. For creating a contra-variance type there is a special syntax like co-variance as below:

class Garage[-T] 

For creating a contra-variance type, we need to use (-) minus/hyphen symbol with type parameter similar to co-variance and all position restrictions are applied but different from co-variance. Let’s create an example of contra-variance:

class Vehicle
class Car extends Vehicle
class Lamborghini extends Car

class Truck extends Vehicle
class FireTruck extends Truck

class TrainingSchool[-T] {
	def driveTheVehicle(vehicle : T) : Vehicle = vehicle.asInstanceOf[Vehicle] 
   // Using asInstanceOf just for example perspective
}

def driveTheCar(carSchool : TrainingSchool[Car], car : Car): Vehicle =  {
	carSchool.driveTheVehicle(car)
}

val carSchool = new TrainingSchool[Car]
val vehicleSchool = new TrainingSchool[Vehicle]
val lamborginiSchool = new TrainingSchool[Lamborghini]

driveTheCar(carSchool, new Car) // compiles and run successfully
res0: Vehicle = Car@4bd5849e

driveTheCar(vehicleSchool, new Car) // Mysterious but compiles and run successfully
res1:  Vehicle = Car@4832f03b

driveTheCar(lamborginiSchool, new Car) // Mysterious but compile time error
<console>:21: error: type mismatch;
 found   : TrainingSchool[Lamborghini]
 required: TrainingSchool[Car]
       driveTheCar(lamborginiSchool, new Car)

As in the example, some mysterious things are happend from co-varaince experience. While we passing supertype of TrainingSchool[Vehicle] is TrainingSchool[Car] to driveTheCar(TrainingSchool[Car], Car) method, the code compiles and run successfully but for passing subtype TrainingSchool[Lamborghini] , we are getting compile time error.

This what we discussed in contra-variance earlier definition, where we mentioned inheritance arrows are actually flipped, now TrainingSchool[Vehicle] type object is a subclass of TrainingSchool[Car] type object but not actually hierarchy effects like Vehicle is a still subclass of Car. This is the reason, we can pass TrainingSchool[Vehicle] type object to driveTheCar(TrainingSchool[Car], Car) method but not TrainingSchool[Lamborghini] type object. There are rules, which we will explore step by step.

Like co-variance, in contra-variance, we have important catch or you can say rule

While creating a generic type with the contra-variance flag (-), the positions of the type parameter are only valid at methods arguments location as below:

class TrainingSchool[-T] {
	def driveTheVehicle(vehicle : T): Vehicle //Valid position
	def washTheVehicle: T // Invalid position, compile time error
}

Contra-Variance FAQ

Why it allows to using type parameter T at method argument location not return type location?

Let's explore the answer via code as below:

class TrainingSchool[-T] {
	def washTheVehicle: T // Suppose this allows and compiles
}

// while creating TrainingSchool[Car] type object 
class TrainingSchool[Car] {
	def washTheVehicle: Car // Suppose this allows and compiles
} 

// while creating TrainingSchool[Vehicle] type object 
class TrainingSchool[Vehicle] {
	def washTheVehicle: Vehicle // Suppose this allows and compiles 
} 

def carGarage(carSchool : TrainingSchool[Car]): Car = {
	carSchool.washTheVehicle
}

val carSchool = new TrainingSchool[Car]
val vehicleSchool = new TrainingSchool[Vehicle]
val lamborginiSchool = new TrainingSchool[Lamborghini]

val car: Car = driveTheCar(carSchool) // Compile and run because it reurns Car
val car : Car = driveTheCar(vehicleSchool) 
// Problem it return object of Vehicle and according to polymophsim this is invalid 
//for catch supertype object into subtype.

In case of passing vehicleSchool type object, at runtime washTheVehicle method from TrainingSchool[Vehicle] type is called, which returns Vehicle type object which is technical invalid to catch supertype object to subtype according to polymorphism, that’s why compiler gives us an error for using type parameter at return type position in conta-varaince.

Why it doesn’t allow us to pass subtype?

If it allows to passing subtype than we are generating the same scenario, which we discussed in co-variance where it doesn’t allow to declaring the type parameter at method return type position, not in method arguments position. Let's take another example:

class TrainingSchool[-T] {
	def driveTheVehicle(vehicle : T) : Vehicle = vehicle.asInstanceOf[Vehicle] 
}

// while creating TrainingSchool[Car] type object 
class TrainingSchool[Car] {
	def driveTheVehicle(vehicle : Car) : Vehicle = vehicle.asInstanceOf[Vehicle] 
}

// while creating TrainingSchool[Vehicle] type object 
class TrainingSchool[Vehicle] {
	def driveTheVehicle(vehicle : Vehicle) : Vehicle = vehicle.asInstanceOf[Vehicle] 
}

// while creating TrainingSchool[Lamborghini] type object 
class TrainingSchool[Lamborghini] {
	def driveTheVehicle(vehicle : Lamborghini) : Vehicle = vehicle.asInstanceOf[Vehicle] 
}

def driveTheCar(carSchool : TrainingSchool[Car], car : Car): Vehicle =  {
	carSchool.driveTheVehicle(car)
}

val lamborgini : Vehicle = driveTheCar(lamborginiSchool, new Jaguar) // Suppose it compiles

Method call driveTheCar(lamborginiSchool, new Jaguar) could generate runtime exceptions because TrainingSchool[Car] driveTheVehicle method could be using Lamborghini driving features which are not available in Jaguar.

How it works for passing the supertype object?

Let's jump to the code:

class TrainingSchool[-T] {
	def driveTheVehicle(vehicle : T) : Vehicle = vehicle.asInstanceOf[Vehicle] 
}

// while creating TrainingSchool[Car] type object 
class TrainingSchool[Car] {
	def driveTheVehicle(vehicle : Car) : Vehicle = vehicle.asInstanceOf[Vehicle] 
}

// while creating TrainingSchool[Vehicle] type object 
class TrainingSchool[Vehicle] {
	def driveTheVehicle(vehicle : Vehicle) : Vehicle = vehicle.asInstanceOf[Vehicle] 
}

def driveTheCar(carSchool : TrainingSchool[Car], car : Car): Vehicle =  {
	carSchool.driveTheVehicle(car)
}

driveTheCar(vehicleSchool, new Car) // compiles and execute fine

The reason driveTheCar(vehicleSchool, new Car) method call works, because compiler knows during runtime, TrainingSchool[Vehicle] driveTheVehicle method call, which is using the features of Vehicle and compiler knows, Car is a subclass of Vehicle, that means all of the Vehicles features are also available in Car, so, there could be no problem if someone passing supertype object. This is the reason, compiles allows to passing supertype in contra-variance.

Last updated