When using inheritance to achieve ad-hoc polymorphism we may need to heavily pollute the interface of our value objects.
Assume we want to implement a Real and a Complex number. Without any functionality, this is as simple as writing
case class Real(value: Double)
case class Complex(real: Double, imaginary: Double)
Now assume we want to implement addition of
- Two real numbers
- A real and a complex number
- Two complex numbers
A solution using inheritance (Edit: Actually, I am not sure if this can be called inheritance since the method add
in the traits has no implementation. However, in that regard, the example doesn't differ from Erik Orheim's example) could look like this:
trait AddableWithReal[A] {
def add(other: Real): A
}
trait AddableWithComplex[A] {
def add(other: Complex): A
}
case class Real(value: Double) extends AddableWithComplex[Complex] with AddableWithReal[Real] {
override def add(other: Complex): Complex = Complex(value + other.real, other.imaginary)
override def add(other: Real): Real = Real(value + other.value)
}
case class Complex(real: Double, imaginary: Double) extends AddableWithComplex[Complex] with AddableWithReal[Complex] {
override def add(other: Complex): Complex = Complex(real + other.real, imaginary + other.imaginary)
override def add(other: Real): Complex = Complex(other.value + real, imaginary)
}
Because the implementation of add is tightly coupled with Real
and Complex
, we have to enlarge their interfaces each time a new type is added (e.g., integers) and each time a new operation is needed (e.g., subtraction).
Type classes provide one way to decouple the implementation from the types. For example, we can define the trait
trait CanAdd[A, B, C] {
def add(a: A, b: B): C
}
and separately implement the addition using implicits
object Implicits {
def add[A, B, C](a: A, b: B)(implicit ev: CanAdd[A, B, C]): C = ev.add(a, b)
implicit object CanAddRealReal extends CanAdd[Real, Real, Real] {
override def add(a: Real, b: Real): Real = Real(a.value + b.value)
}
implicit object CanAddComplexComplex extends CanAdd[Complex, Complex, Complex] {
override def add(a: Complex, b: Complex): Complex = Complex(a.real + b.real, a.imaginary + b.imaginary)
}
implicit object CanAddComplexReal extends CanAdd[Complex, Real, Complex] {
override def add(a: Complex, b: Real): Complex = Complex(a.real + b.value, a.imaginary)
}
implicit object CanAddRealComplex extends CanAdd[Real, Complex, Complex] {
override def add(a: Real, b: Complex): Complex = Complex(a.value + b.real, b.imaginary)
}
}
This decoupling has at least two benefits
- Prevent pollution of the interfaces of
Real
and Complex
- Allows introducing new
CanAdd
-functionality without the ability to modify the source code of the classes that can be added
For example, we can define CanAdd[Int, Int, Int]
to add two Int
values without modifying the Int
class:
implicit object CanAddIntInt extends CanAdd[Int, Int, Int] {
override def add(a: Int, b: Int): Int = a + b
}