TODO: introduction
Note: the examples below use the following algebraic data type:
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(height: Double, width: Double) extends Shape
List[+A]
is covariant. This means we can always cast a List[Circle]
into a List[Shape]
:
val circles = List(Circle(2.3), Circle(4.2), Circle(0.4))
// circles: List[Circle] = List(Circle(2.3), Circle(4.2), Circle(0.4))
val shapes: List[Shape] = circles
// shapes: List[Shape] = List(Circle(2.3), Circle(4.2), Circle(0.4))
But the converse is not true:
val shapesAsCircles: List[Circle] = shapes
// type mismatch;
// found : List[Shape]
// required: List[Circle]
// val shapesAsCircles: List[Circle] = shapes
// ^
Alternatively, we can convert a List[Circle]
to a List[Shape]
by using map
. We pass map
the identity function, which compiles because the compiler can coerce the subtype to the supertype:
def toShape1(circles: List[Circle]): List[Shape] =
circles.map(circle => circle)
toShape1(circles)
// res1: List[Shape] = List(Circle(2.3), Circle(4.2), Circle(0.4))
Or we can coerce explicitly to the supertype ourselves:
def toShape2(circles: List[Circle]): List[Shape] =
circles.map(circle => circle: Shape)
toShape2(circles)
// res2: List[Shape] = List(Circle(2.3), Circle(4.2), Circle(0.4))
Rather than using a type coercion, we can use the compiler's own evidence of the subtype relationship, and this evidence is an actual function Circle => Shape
!
def toShape3(circles: List[Circle])(implicit ev: Circle <:< Shape): List[Shape] =
circles.map(ev)
toShape3(circles)
// res3: List[Shape] = List(Circle(2.3), Circle(4.2), Circle(0.4))
The
<:<
type is typically written as a type with infix type parameters (A <:< B
), but it may be easier to understand when written more traditionally using postfix type parameters, i.e., the type<:<[A, B]
, because the definition of<:<
is, more or less,class <:<[A, B] extends Function1[A, B]
. So the typeA <:< B
is really aA => B
function!
In cats
, this operation is abstracted as Functor.widen
:
import cats.implicits._
circles.widen[Shape]
// res4: List[Shape] = List(Circle(2.3), Circle(4.2), Circle(0.4))
TODO: explain we are only showing the OOP and FP methods as equivalent, you don't normally actually invoke map
repeat
repeat
Covariance: OOP (theory)
F[+A]
impliesF[B] <: F[A]
ifB <: A
. (craft) We can coerceF[B]
values toF[A]
, at compile time, if one can coerce aB
to anA
.
Covariance: FP (craft) To be able to
map
anF[B]
to anF[A]
is sufficient to achieve the OOP coercion, because everyB
is also anA
. (theory) AFunctor[F[_]]
is covariance.