【问题标题】:Why prefer Typeclass over Inheritance?为什么更喜欢 Typeclass 而不是 Inheritance?
【发布时间】:2015-08-13 00:58:57
【问题描述】:

根据Erik Osheim's slide,他说继承可以解决与 typeclass 相同的问题, 但是提到继承有个问题叫:

脆弱的继承噩梦

说继承是

将多态性与成员类型紧密耦合

他是什么意思?


在我看来,继承擅长扩展,无论是改变现有类型的实现还是向接口添加新的成员类型(子类型)。

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 = ???
}

现在就类型类而言:

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 ,我是否遗漏了什么?

【问题讨论】:

  • 如果您想将Int 添加到您的Foo 家庭怎么办?你是怎么做到的?
  • @Wei-ChingLin 这会给error: illegal inheritance from final class Int
  • @Kolmar 好,这是个问题。但是为什么Int 必须是最终的?
  • 继承的另一个问题是它只是第一个参数的参数(这是隐含的,因为 OOP 中的第一个参数是对象本身)。类型类在多个参数上是参数化的。想想+
  • 我认为它是“静态”多次调度,根本不是继承的一个非常关键的原因。

标签: scala inheritance functional-programming typeclass


【解决方案1】:

当使用继承来实现临时多态性时,我们可能需要严重污染值对象的接口。

假设我们要实现一个实数和一个复数。没有任何功能,这就像写一样简单

case class Real(value: Double)

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

现在假设我们要实现加法

  • 两个实数
  • 实数和复数
  • 两个复数

使用继承的解决方案(编辑:实际上,我不确定这是否可以称为继承,因为特征中的方法add没有实现。但是,在这方面,示例与 Erik Orheim 的示例没有区别)可能如下所示:

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 的实现与RealComplex 紧密耦合,所以每次添加新类型(例如,整数)和每次需要新操作(例如,减法)时,我们都必须扩大它们的接口)。

类型类提供了一种将实现与类型分离的方法。例如,我们可以定义 trait

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

并使用隐式分别实现加法

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

这种脱钩至少有两个好处

  1. 防止RealComplex的接口被污染
  2. 允许引入新的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
}

【讨论】:

  • ev.add(a,b)代替implicitly[CanAdd[A, B, C]].add(a, b)会更短吗?
  • @VictorMoroz 你是对的。一开始我没有将ev 作为参数,但上下文边界仅适用于一个通用参数。谢谢你的提示;我更新了答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-03-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-09-28
  • 2023-01-16
相关资源
最近更新 更多