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 can help avoid this allowing both new data type implementations and new behaviors for existing data types to be added 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
}

References

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 -s https://earldouglas.com/posts/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