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 =
[Area[A]].area(x)
implicitly}
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 =
[Perimeter[A]].perimeter(x)
implicitly}
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
}
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))}")
This file is literate Scala, and can be run using Codedown:
$ curl 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