Type Classes and the Expression Problem

The Expression Problem describes a trade-off between OOP and FP, in which adding functionality requires changing existing code:

New Functionality OOP FP
Add a new data type implementation No Changes Changes
Add behavior to an existing data type Changes No Changes

Type classes help avoid this trade-off by allowing us to add both new data type implementations and new behaviors for existing data types without needing to change existing code.

Given a Shape type and a couple of implementations:

trait Shape
case class Circle(radius: Double) extends Shape
case class Square(side: Double) extends Shape

We can write a type class to compute the area of each shape:

trait Area[A] {
  def area(x: A): Double
}

object Area {
  def of[A : Area](x: A): Double =
    implicitly[Area[A]].area(x)
}

implicit object CircleArea extends Area[Circle] {
  override def area(x: Circle): Double = 3.14 * x.radius * x.radius
}

implicit object SquareArea extends Area[Square] {
  override def area(x: Square): Double = x.side * x.side
}

We can add a new data type implementation Triangle, and an Area instance for it:

case class Triangle(base: Double, height: Double) extends Shape

implicit object TriangleArea extends Area[Triangle] {
  override def area(x: Triangle): Double = 0.5 * x.base * x.height
}

We can also add new behavior to compute the perimeter of each of our shapes:

trait Perimeter[A] {
  def perimeter(x: A): Double
}

object Perimeter {
  def of[A : Perimeter](x: A): Double =
    implicitly[Perimeter[A]].perimeter(x)
}

implicit object CirclePerimeter extends Perimeter[Circle] {
  override def perimeter(x: Circle): Double = 3.14 * x.radius * 2
}

implicit object SquarePerimeter extends Perimeter[Square] {
  override def perimeter(x: Square): Double = x.side * 4
}

Example

println(s"Area.of(Circle(2)): ${Area.of(Circle(2))}")
println(s"Area.of(Square(3)): ${Area.of(Square(3))}")
println(s"Area.of(Triangle(4, 5)): ${Area.of(Triangle(4, 5))}")
println(s"Perimeter.of(Circle(2)): ${Perimeter.of(Circle(2))}")
println(s"Perimeter.of(Square(3)): ${Perimeter.of(Square(3))}")

Demo

This file is literate Scala, and can be run using Codedown:

$ curl https://earldouglas.com/type-classes/expression.md |
  codedown scala | xargs -0 scala -nc -e
Area.of(Circle(2)): 12.56
Area.of(Square(3)): 9.0
Area.of(Triangle(4, 5)): 10.0
Perimeter.of(Circle(2)): 12.56
Perimeter.of(Square(3)): 12.0

References