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:
Below code never compile, this is for explanation purpose only.
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:
Below code never compile, this is for explanation purpose only.
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:
Below code never compile, this is for explanation purpose only.
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