Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
780 views
in Technique[技术] by (71.8m points)

scala - Why prefer Typeclass over Inheritance?

According to this Erik Osheim's slide, he says the inheritance can solve the same problem as typeclass would, but mentions that inheritance has a problem called:

brittle inheritance nightmare

and says the inheritance is

tightly coupling the polymorphism to the member types

What is he means?


In my opinion, Inheritance is good at extension, either to change implementation of existing type or add new member type(subtype) to interface.

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

Now in terms of 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]
   …
}

I don't see any reason to prefer Typeclass , am I missing something?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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

  1. Prevent pollution of the interfaces of Real and Complex
  2. 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
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...