Taken from Essential Scala by Noel Welsh and Dave Gurnell
Invariant: Foo[T]
: Foo[A]
and Foo[B]
are unrelated regardless
of the relationship between A
and B
Covariant: Foo[+T]
-> Foo[A]
is a supertype ofFoo[B]
if A
is a supertype of B
.
Contravariant: Foo[-T]
-> Foo[A]
is a subtype of Foo[B]
if A
is a supertype of B
.
val shape: Shape = ???
val circle: Circle = ???
val shapeWriter: JsonWriter[Shape] = ???
val circleWriter: JsonWriter[Circle] = ???
def format[A](value: A, writer: JsonWriter[A]): Json =
writer.write(value)
We can combine circle
with either writer
because all Circle
s are Shape
s.
Conversely, we can’t combine shape
with circleWriter
because not all Shape
s are Circle
s.
Using contravariance. JsonWriter[Shape]
is a subtype of JsonWriter[Circle]
because Circle
is a subtype of Shape
.
This means we can use shapeWriter
anywhere we expect to see a JsonWriter[Circle]
.
If A
of type T
is a B
or C
, and C
is not generic, write:
sealed trait A[+T]
final case class B[T](t: T) extends A[T]
final case object C extends A[Nothing]
If A
of a covariant type T
and a method f
of A
complains that T
is used in a contravariant position,
introduce a type TT >: T
in f
:
case class A[+T] {
def f[TT >: T](t: TT): A[TT]
}
Taken from Essential Scala by Noel Welsh and Dave Gurnell
What’s the relationship between the terms “contravariance”, “invariance”, and “covariance” and these different kinds of functor?
If you recall from Sec on 1.6.1, variance affects subtyping, which is essentially our ability to use a value of one type in place of a value of another type without breaking the code.
Subtyping can be viewed as a conversion. If B
is a subtype of A
, we can always convert a B
to an A
.
Equivalently we could say that B
is a subtype of A
if there exists a function A => B
. A standard covariant functor captures exactly this. If F
is a covariant functor, wherever we have an F[A]
and a conversion A => B
we can always convert to an F[B]
.
A contravariant functor captures the opposite case. If F
is a contravariant functor, whenever we have a F[A]
and a conversion B => A
we can convert to an F[B]
.
Finally, invariant functors capture the case where we can convert from F[A]
to F[B]
via a func on A => B
and vice versa via a func on B => A
.