Skip to content

Instantly share code, notes, and snippets.

@arosien
Created November 2, 2018 16:35
Show Gist options
  • Save arosien/750aca8e75ee9cac7e157be39f91602f to your computer and use it in GitHub Desktop.
Save arosien/750aca8e75ee9cac7e157be39f91602f to your computer and use it in GitHub Desktop.

Variance: OOP vs. FP

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

Covariance: OOP

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
//                                     ^

Covariance: FP

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 type A <:< B is really a A => 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

Contravariance: OOP

repeat

Contravariance: FP

repeat

Summary

Covariance: OOP (theory) F[+A] implies F[B] <: F[A] if B <: A. (craft) We can coerce F[B] values to F[A], at compile time, if one can coerce a B to an A.

Covariance: FP (craft) To be able to map an F[B] to an F[A] is sufficient to achieve the OOP coercion, because every B is also an A. (theory) A Functor[F[_]] is covariance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment