Scala contravariance - real life example
Short answer that might help people who were super confused like me and didn't want to read these long winded examples:
Imagine you have 2 classes Animal
, and Cat
, which extends Animal
. Now, imagine that you have a type Printer[Cat]
, that contains the functionality for printing Cat
s. And you have a method like this:
def print(p: Printer[Cat], cat: Cat) = p.print(cat)
but the thing is, that since Cat
is an Animal
, Printer[Animal]
should also be able to print Cat
s, right?
Well, if Printer[T]
were defined like Printer[-T]
, i.e. contravariant, then we could pass Printer[Animal]
to the print
function above and use its functionality to print cats.
This is why contravariance exists. Another example, from C#
, for example, is the class IComparer
which is contravariant as well. Why? Because we should be able to use Animal
comparers to compare Cat
s, too.
An example based on a real-world event-driven software system. Such a system is based on broad categories of events, like events related to the functioning of the system (system events), events generated by user actions (user events) and so on.
A possible event hierarchy:
trait Event
trait UserEvent extends Event
trait SystemEvent extends Event
trait ApplicationEvent extends SystemEvent
trait ErrorEvent extends ApplicationEvent
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 been fired.
trait Sink[-In] {
def notify(o: In)
}
As a consequence of marking the type parameter with the -
symbol, the Sink 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:
def appEventFired(e: ApplicationEvent, s: Sink[ApplicationEvent]): Unit = {
// do some processing related to the event
// notify the event sink
s.notify(e)
}
def errorEventFired(e: ErrorEvent, s: Sink[ErrorEvent]): Unit = {
// do some processing related to the event
// notify the event sink
s.notify(e)
}
A couple of hypothetical Sink implementations.
trait SystemEventSink extends Sink[SystemEvent]
val ses = new SystemEventSink {
override def notify(o: SystemEvent): Unit = ???
}
trait GenericEventSink extends Sink[Event]
val ges = new GenericEventSink {
override def notify(o: Event): Unit = ???
}
The following method calls are accepted by the compiler:
appEventFired(new ApplicationEvent {}, ses)
errorEventFired(new ErrorEvent {}, ges)
appEventFired(new ApplicationEvent {}, ges)
Looking at the series of calls you notice that it is possible to call a method expecting a Sink[ApplicationEvent]
with a Sink[SystemEvent]
and even with a Sink[Event]
. Also, you can call the method expecting a Sink[ErrorEvent]
with a Sink[Event]
.
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 generic.
Conclusion
This example has been described in a series of articles about variance found on my blog
In the end, I think it helps to also understand the theory behind it...
This example is from the last project I was working on. Say you have a type-class PrettyPrinter[A]
that provides logic for pretty-printing objects of type A
. Now if B >: A
(i.e. if B
is superclass of A
) and you know how to pretty-print B
(i.e. have an instance of PrettyPrinter[B]
available) then you can use the same logic to pretty-print A
. In other words, B >: A
implies PrettyPrinter[B] <: PrettyPrinter[A]
. So you can declare PrettyPrinter[A]
contravariant on A
.
scala> trait Animal
defined trait Animal
scala> case class Dog(name: String) extends Animal
defined class Dog
scala> trait PrettyPrinter[-A] {
| def pprint(a: A): String
| }
defined trait PrettyPrinter
scala> def pprint[A](a: A)(implicit p: PrettyPrinter[A]) = p.pprint(a)
pprint: [A](a: A)(implicit p: PrettyPrinter[A])String
scala> implicit object AnimalPrettyPrinter extends PrettyPrinter[Animal] {
| def pprint(a: Animal) = "[Animal : %s]" format (a)
| }
defined module AnimalPrettyPrinter
scala> pprint(Dog("Tom"))
res159: String = [Animal : Dog(Tom)]
Some other examples would be Ordering
type-class from Scala standard library,Equal
, Show
(isomorphic to PrettyPrinter
above), Resource
type-classes from Scalaz etc.
Edit:
As Daniel pointed out, Scala's Ordering
isn't contravariant. (I really don't know why.) You may instead consider scalaz.Order
which is intended for the same purpose as scala.Ordering
but is contravariant on its type parameter.
Addendum:
Supertype-subtype relationship is but one type of relationship that can exist between two types. There can be many such relationships possible. Let's consider two types A
and B
related with function f: B => A
(i.e. an arbitrary relation). Data-type F[_]
is said to be a contravariant functor if you can define an operation contramap
for it that can lift a function of type B => A
to F[A => B]
.
The following laws need to be satisfied:
x.contramap(identity)
==x
x.contramap(f).contramap(g)
==x.contramap(f compose g)
All of the data types discussed above (Show
, Equal
etc.) are contravariant functors. This property lets us do useful things such as the one illustrated below:
Suppose you have a class Candidate
defined as:
case class Candidate(name: String, age: Int)
You need an Order[Candidate]
which orders candidates by their age. Now you know that there is an Order[Int]
instance available. You can obtain an Order[Candidate]
instance from that with the contramap
operation:
val byAgeOrder: Order[Candidate] =
implicitly[Order[Int]] contramap ((_: Candidate).age)
In my opinion, the two most simple examples after Function
are ordering and equality. However, the first is not contra-variant in Scala's standard library, and the second doesn't even exist in it. So, I'm going to use Scalaz equivalents: Order and Equal.
Next, I need some class hierarchy, preferably one which is familiar and, of course, it both concepts above must make sense for it. If Scala had a Number
superclass of all numeric types, that would have been perfect. Unfortunately, it has no such thing.
So I'm going to try to make the examples with collections. To make it simple, let's just consider Seq[Int]
and List[Int]
. It should be clear that List[Int]
is a subtype of Seq[Int]
, ie, List[Int] <: Seq[Int]
.
So, what can we do with it? First, let's write something that compares two lists:
def smaller(a: List[Int], b: List[Int])(implicit ord: Order[List[Int]]) =
if (ord.order(a,b) == LT) a else b
Now I'm going to write an implicit Order
for Seq[Int]
:
implicit val seqOrder = new Order[Seq[Int]] {
def order(a: Seq[Int], b: Seq[Int]) =
if (a.size < b.size) LT
else if (b.size < a.size) GT
else EQ
}
With these definitions, I can now do something like this:
scala> smaller(List(1), List(1, 2, 3))
res0: List[Int] = List(1)
Note that I'm asking for an Order[List[Int]]
, but I'm passing a Order[Seq[Int]]
. This means that Order[Seq[Int]] <: Order[List[Int]]
. Given that Seq[Int] >: List[Int]
, this is only possible because of contra-variance.
The next question is: does it make any sense?
Let's consider smaller
again. I want to compare two lists of integers. Naturally, anything that compares two lists is acceptable, but what's the logic of something that compares two Seq[Int]
being acceptable?
Note in the definition of seqOrder
how the things being compared becomes parameters to it. Obviously, a List[Int]
can be a parameter to something expecting a Seq[Int]
. From that follows that a something that compares Seq[Int]
is acceptable in place of something that compares List[Int]
: they both can be used with the same parameters.
What about the reverse? Let's say I had a method that only compared ::
(list's cons), which, together with Nil
, is a subtype of List
. I obviously could not use this, because smaller
might well receive a Nil
to compare. It follows that an Order[::[Int]]
cannot be used instead of Order[List[Int]]
.
Let's proceed to equality, and write a method for it:
def equalLists(a: List[Int], b: List[Int])(implicit eq: Equal[List[Int]]) = eq.equal(a, b)
Because Order
extends Equal
, I can use it with the same implicit above:
scala> equalLists(List(4, 5, 6), List(1, 2, 3)) // we are comparing lengths!
res3: Boolean = true
The logic here is the same one. Anything that can tell whether two Seq[Int]
are the same can, obviously, also tell whether two List[Int]
are the same. From that, it follows that Equal[Seq[Int]] <: Equal[List[Int]]
, which is true because Equal
is contra-variant.