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 ShapeList[+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 <:< Bis really aA => Bfunction!
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 aBto anA.
Covariance: FP (craft) To be able to
mapanF[B]to anF[A]is sufficient to achieve the OOP coercion, because everyBis also anA. (theory) AFunctor[F[_]]is covariance.