Inheritance and code reuse in stackable traits

2020-02-07 02:49发布

问题:

In this simplified experiment, I want to be able to quickly build a class with stackable traits that can report on what traits were used to build it. This reminds me strongly of the decorator pattern, but I'd prefer to have this implemented at compile time rather than at runtime.

Working Example with Redundant Code

class TraitTest {
  def report(d: Int) : Unit = {
    println(s"At depth $d, we've reached the end of our recursion")
  }
}

trait Moo  extends TraitTest {
  private def sound = "Moo"
  override def report(d: Int) : Unit = {
    println(s"At depth $d, I make the sound '$sound'")
    super.report(d+1)
  }
}
trait Quack extends TraitTest {
  private def sound = "Quack"
  override def report(d: Int) : Unit = {
    println(s"At depth $d, I make the sound '$sound'")
    super.report(d+1)
  }
}

Executing (new TraitTest with Moo with Quack).report(0) would then report:

> At depth 0, I make the sound 'Quack'
  At depth 1, I make the sound 'Moo'
  At depth 2, we've reached the end of our recursion 

Unfortunately, there's a lot of redundant code in there that makes my eye twitch. My attempt at cleaning it up leads me to:

Non-working Example without Redundant Code

class TraitTest {
  def report(d: Int) : Unit = {
    println(s"At depth $d, we've reached the end of our recursion")
  }
}

abstract trait Reporter extends TraitTest {
  def sound : String
  override def report(d: Int) : Unit = {
    println(s"At depth $d, I make the sound '${sound}'")
    super.report(d+1)
  }
}

trait Moo extends Reporter {
  override def sound = "Moo"
}
trait Quack extends Reporter{
  override def sound = "Quack"
}

When we once again execute (new TraitTest with Moo with Quack).report(0), we now see:

> At depth 0, I make the sound 'Quack'
  At depth 1, we've reached the end of our recursion

Question 1: Where did the line for 'Moo' go?

I'm guessing that Scala only sees override def report(d: Int) the one time, and therefore only puts it in the inheritance chain once. I'm grasping at straws, but if that's the case, how can I work around that?

Question 2: How can each concrete trait supply a unique sound?

After solving the first question, I would assume the results of executing (new TraitTest with Moo with Quack).report(0) would look something like the following, due to how the inheritance of sound would work.

> At depth 0, I make the sound 'Quack'
  At depth 1, I make the sound 'Quack'
  At depth 2, we've reached the end of our recursion  

How can we make it so that each trait uses the sound specified in it's implementation?

回答1:

A trait can be inherited at most once. It is basically just a java interface extended with non-abstract methods by the scala compiler.

When a concrete class is being constructed, all inherited traits get linearized so you have a defined order of your stacked traits. If you inherit a trait twice, just the first occurrence will be included. So in

class C1 extends A with B 
class C2 extends C1 with X with B

The position of the B trait in the linearized inheritance stack will be after A but before C1 and X. The second B mixin is ignored.

Even tricks like using type parameters will not work due to erasure. So this will not work:

class X extends A with T[Int] with T[String]

(This would work on platforms without erasure such as .NET)

Some advice from personal experience

I think while stacking traits is sometimes a nice feature, if you have a large inheritance hierarchy with stacked traits it can be something of a maintenance nightmare. Functionality depends on the order in which traits are being mixed in, so just a simple change in the order of traits can break your program.

Also, using inheritance for class hierarchies of immutable objects pretty much requires the use of an explicit self-type type parameter, which brings its another level of complexity. See the xxxLike traits in the scala collections for example.

Traits are of course very useful and unproblematic when they are non-overlapping. But in general, the rule favor composition over inheritance is just as true for scala as for other OO languages. Scala gives you powerful tools for inheritance with traits, but it also gives you arguably even more powerful tools for composition (value classes, implicits, the typeclass pattern, ...)

Help with managing large trait hierarchies

  1. There are some tools to enforce a certain order. For example if a method in a trait is not marked override, you can not mix it into a class that already implements the method. And of course if you mark a method as final in a trait, you ensure that it is always "on top". Marking methods final in traits is a very good idea in any case.

  2. If you decide to go with a complex trait hierarchy, you will need a way to inspect the trait order. This exists in the form of scala reflection. See this answer mixin order using reflection.

Example how to get trait order using scala reflection

import scala.reflect.runtime.universe._
class T extends TraitTest with Moo with Quack
scala> typeOf[T].baseClasses
res4: List[reflect.runtime.universe.Symbol] = 
  List(class T, trait Quack, trait Moo, class TraitTest, class Object, class Any)

You will need to include scala-reflect.jar on the classpath though, which is now a separate dependency. I just used a sbt project, added

libraryDependencies += "org.scala-lang" % "scala-reflect" % "2.10.2"

to build.sbt and started sbt console .



回答2:

Here is an example of preferring composition. The amplification logic is refactored.

I find I must use abstract override once or twice a year or else that brain cell will die.

In this example, the animal becomes noisier as you mix in more Noise.

It uses runtime reflection, but of course you could imagine a macro doing something similar. (You'd have to tell it what this is.)

Real code would of course perform more interesting transforms; for instance, a pig noise mixed in after a duck noise would sound like a goose just delivering an egg.

package sounds

trait Sound {
  def sound: String
}

trait Silent extends Sound {
  def sound: String = ""
}

// duck is always funnier
trait Duck extends Silent

object Amplifier {
  import reflect.runtime.currentMirror
  import reflect.runtime.universe._
  def apply[A <: Sound : TypeTag](x: Any): Int = {
    val im = currentMirror reflect x
    val tpe = im.symbol.typeSignature
    var i = -1
    for (s <- tpe.baseClasses) {
      if (s.asClass.toType =:= typeOf[A]) i = 0
      else if (s.asClass.toType <:< typeOf[Noise]) i += 1
    }
    i
  }
}

trait Noise
trait NoisyQuack extends Sound with Noise {
  abstract override def sound: String = super.sound + noise * amplification
  private val noise = "quack"
  private def amplification: Int = Amplifier[NoisyQuack](this)
}
trait NoisyGrunt extends Sound with Noise {
  abstract override def sound: String = super.sound + noise * amplification
  private val noise = "grunt"
  private def amplification: Int = Amplifier[NoisyGrunt](this)
}

object Test extends App {
  val griffin = new Duck with NoisyQuack with NoisyGrunt {
    override def toString = "Griffin"
  }
  Console println s"The $griffin goes ${griffin.sound}"
}


回答3:

I have come with a few modifications which reduce the code repetition and remind the user to call the super.report by forcing him to declare the method as abstract override:

  trait TraitTest {
    def report(d: Int): Unit

    def reportSound(d: Int, sound: => String): Unit = {
      println(s"At depth $d, I make the sound '$sound'")
    }
  }

  trait TraitTestRoot extends TraitTest {
    def report(d: Int): Unit = {
      println(s"At depth $d, we've reached the end of our recursion")
    }
  }

  trait Moo extends TraitTest {
    private def sound = "Moo"

    abstract override def report(d: Int): Unit = {
      reportSound(d, sound)
      super.report(d + 1)
    }
  }

  trait Quack extends TraitTest {
    private def sound = "Quack"

    abstract override def report(d: Int): Unit = {
      reportSound(d, sound)
      super.report(d + 1)
    }
  }

  (new TraitTestRoot with Moo with Quack).report(0)