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
767 views
in Technique[技术] by (71.8m points)

scala - Inheritance and code reuse in stackable traits

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?

See Question&Answers more detail:os

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

1 Answer

0 votes
by (71.8m points)

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 .


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

...