Почему предпочитаете Typeclass над наследством?

В соответствии с этим Erik Osheim slide, он говорит, что наследование может решить ту же проблему, что и typeclass, но упоминает, что наследование имеет проблему:

хрупкий кошмар наследования

и говорит, что наследование

плотное связывание полиморфизма с типами членов

Что он имеет в виду?


На мой взгляд, Inheritance хорош для расширения, либо для изменения реализации существующего типа, либо для добавления нового типа элемента (подтипа) в интерфейс.

trait Foo { def foo }

class A1 extends Foo{
  override def foo: Unit = ???
}

//change the foo implementation of the existing A1
class A2 extends A1 with Foo{  
  override def foo = ???
}

// add new type B1 to Fooable family
class Bb extends Foo{        
  override def foo = ???
}

Теперь в терминах typeclass:

trait Fooable[T] { … }
def foo[T:Fooable](t:T) = …

class Aa {…}
class Bb {…}
object MyFooable {
  implicit object AaIsFooable extends Fooable[Aa]
  implicit object B1IsFooable extends Fooable[Bb]
   …
}

Я не вижу причин предпочитать Typeclass, я что-то упустил?

Ответ 1

При использовании наследования для достижения ad-hoc-полиморфизма нам может потребоваться существенное загрязнение интерфейса наших объектов значений.

Предположим, мы хотим реализовать Реальное и Комплексное число. Без какой-либо функциональности это так же просто, как писать

case class Real(value: Double)

case class Complex(real: Double, imaginary: Double)

Теперь предположим, что мы хотим реализовать добавление

  • Два действительных числа
  • Реальное и комплексное число
  • Два комплексных числа

Решение с использованием наследования ( Изменить: На самом деле, я не уверен, что это можно назвать наследованием, поскольку метод add в чертах не имеет реализации. Однако в этом отношении пример не отличается от примера Эрика Орхейма) может выглядеть так:

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)
}

Поскольку реализация add тесно связана с Real и Complex, мы должны увеличить их интерфейсы каждый раз, когда добавляется новый тип (например, целые числа), и каждый раз, когда требуется новая операция (например, вычитание).

Типы классов предоставляют один из способов отделить реализацию от типов. Например, мы можем определить признак

trait CanAdd[A, B, C] {
  def add(a: A, b: B): C
}

и отдельно реализовать добавление с помощью 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)
  }
}

Эта развязка имеет как минимум два преимущества

  • Предотвращение загрязнения интерфейсов Real и Complex
  • Позволяет вводить новую CanAdd -функцию без возможности изменять исходный код классов, которые могут быть добавлены

Например, мы можем определить CanAdd[Int, Int, Int], чтобы добавить два значения Int без изменения класса Int:

implicit object CanAddIntInt extends CanAdd[Int, Int, Int] {
  override def add(a: Int, b: Int): Int = a + b
}