This this is the last part out of a series of articles that examine the concept of variance. Following the second part that dealt with covariance, the focus now will be on contravariance. A thorough reading of the first part is recommended for getting familiar with the terminology and the basic concepts.
In the same vein as in the previous articles, I will continue with a concrete demonstration. For the sake of convenience, I will adopt the same scenario, only this time using a different perspective.
So now the programmers working on the event-driven system need to find a way to
register/process the events generated in the system. They will create a trait,
Sink, that is used to mark components in need to be notified when an event has
As a consequence of marking the type parameter with the
- symbol, the
type became contravariant.
A possible way to notify interested parties that an event happened is to write a method and to pass it the corresponding event. This method will hypothetically do some processing and then it will take care of notifying the event sink:
A couple of hypothetical
The following method calls are accepted by the compiler:
Looking at the series of calls you notice that it is possible to call a method
Sink[ApplicationEvent] with a
Sink[SystemEvent] and even with
Also, you can call the method expecting a
Sink[ErrorEvent] with a
How is this possible?
By replacing invariance
with a contravariance constraint, a
Sink[SystemEvent] becomes a subtype of
Sink[ApplicationEvent]. Therefore, contravariance can also be thought of as a
‘widening’ relationship, since types are ‘widened’ from more specific to more
Like in the case of covariance, the contravariance annotation makes it possible to create a type hierarchy between parameterized types that is parallel to the type hierarchy of the types used as parameters.
Only in this case, the direction of inheritance between parameterized
Sink[Event] is the inverse of the
direction of inheritance between
Event, as depicted in the
Hence the name, contravariance.
Formally, if a type is covariant, then, assuming existing types
T[B] conforms to (is assignable to)
B must be the super type
Conformance follows the direction of inheritance between the parameterized
types, therefore, the following statement would also compile:
val v: Sink[UserEvent] = ges
This article explained and demonstrated covariance with the help of a practical example. Next articles will focus on other aspects related to the type system.