Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 408
moatra Avatar asked Jul 06 '13 06:07

moatra


People also ask

What is Stackable traits pattern in Scala?

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.

What are traits in Scala?

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.

Why do we need traits in Scala?

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.


2 Answers

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 .

like image 198
Rüdiger Klaehn Avatar answered Oct 13 '22 10:10

Rüdiger Klaehn


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}"
}
like image 25
som-snytt Avatar answered Oct 13 '22 12:10

som-snytt