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?
The stackable traits design pattern is based on mixin composition—something we became familiar with in the early chapters of this book. We usually have an abstract class or a trait that defines an interface, a base implementation, and traits that extend the abstract class to stack modifications on it.
A Trait is a concept pre-dominantly used in object-oriented programming, which can extend the functionality of a class using a set of methods. Traits are similar in spirit to interfaces in Java programming language. Unlike a class, Scala traits cannot be instantiated and have no arguments or parameters.
Traits are used to define object types by specifying the signature of the supported methods. Scala also allows traits to be partially implemented but traits may not have constructor parameters. A trait definition looks just like a class definition except that it uses the keyword trait.
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
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.
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 .
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}"
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With