I am thinking of the following example to illustrate why contravariance is useful.
Let's consider a GUI framework with Widgets
, Events
, and Event Listeners
.
abstract class Event;
class KeyEvent extends Event
class MouseEvent extends Event
trait EventListener[-E] { def listen(e:E) }
Let Widgets
define the following methods:
def addKeyEventListener(listener:EventListener[KeyEvent])
def addMouseEventListener(listener:EventListener[MouseEvent])
These methods accept only "specific" event listeners, which is fine. However I would like to define also "kitchen-sink" listeners, which listen to all events, and pass such listeners to the "add listener" methods above.
For instance, I would like to define LogEventListener
to log all incoming events
class LogEventListener extends EventListener[Event] {
def listen(e:Event) { log(event) }
}
Since the trait EventListener
is contravariant in Event
we can pass LogEventListener
to all those "add listener" methods without losing their type safety.
Does it make sense ?
In C#, covariance and contravariance enable implicit reference conversion for array types, delegate types, and generic type arguments. Covariance preserves assignment compatibility and contravariance reverses it.
In differential geometry, the components of a vector relative to a basis of the tangent bundle are covariant if they change with the same linear transformation as a change of basis. They are contravariant if they change by the inverse transformation.
A type can be declared contravariant in a generic interface or delegate only if it defines the type of a method's parameters and not of a method's return type. In , ref , and out parameters must be invariant, meaning they are neither covariant nor contravariant.
Covariance in C# Covariance enables you to pass a derived type where a base type is expected. Co-variance is like variance of the same kind. The base class and other derived classes are considered to be the same kind of class that adds extra functionalities to the base type.
It makes sense to me, anyway. And it is also one of the most intuitive examples I have seen: something which listens to all events naturally will listen to key events or mouse events.
Makes sense to me too. As a rule of thumb, a parametrized type Type[A]
should be contravariant with respect to its type parameter A
each time it is meant to accept instances of A
to do something with them by means of accepting them as parameters.
For instance, the Java type Comparator[T]
, if it had been defined in Scala, would have been contravariant: a Comparator[Any]
should be a subtype of Comparator[String]
, as it can compare all objects a Comparator[String]
can compare, and more. The most general example is the argument types of the FunctionX
classes, which are all contravariant.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With