1. Introduction

Traits are reusable components that can be used to extend the behavior of classes. They are similar to interfaces and contain both abstract and concrete methods and properties.

In this tutorial, let’s see how to create and extend traits.

2. Example

Let’s consider a contrived example of modeling a film score.

2.1. Creating and Extending a Trait

A musical score needs to have a composition. Let’s begin by creating a Composition trait:

trait Composition {
  var composer: String

  def compose(): String
}

Now let’s extend the above and create a Score class:

class Score(var composer: String) extends Composition {
  override def compose(): String = s"The score is composed by $composer"
}

As we can see, when we extend a trait, we need to provide the implementations of all the abstract members (both methods and properties). If we want to skip the implementation, we would have to make the inheriting class abstract.

2.2. Extending Multiple Traits

A Score also needs sound production. Let’s create a trait for the same:

trait SoundProduction {
  var engineer: String

  def produce(): String
}

Let’s modify the Score class to extend the above trait as well:

class Score(var composer: String, var engineer: String)
  extends Composition with SoundProduction {

  override def compose(): String = s"The score is composed by $composer"

  override def produce(): String = s"The score is produced by $engineer"
}

Notice that when we inherit multiple traits, we can use the keyword extends only for the first trait. For subsequent traits, we need to use the keyword with.

2.3. Extending a Trait in Another Trait

A Composition needs Orchestration and Mixing. So, let’s create a trait for Orchestration:

trait Orchestration {
  var orchestra: String
}

And then Mixing:

trait Mixing {
  var mixer: String
}

Let’s modify the Composition trait to extend both the above traits:

trait Composition extends Orchestration with Mixing {
  var composer: String

  def compose(): String
}

Since Composition is a trait by itself, it’s not mandatory for it to override the abstract members of the parent traits. Let’s override these members in the Score class instead:

class Score(var composer: String,
            var engineer: String,
            var orchestra: String,
            var mixer: String)
  extends Composition with SoundProduction {

  override def compose(): String =
    s"""The score is composed by $composer,
       |Orchestration by $orchestra,
       |Mixed by $mixer""".stripMargin

  override def produce(): String = s"The score is produced by $engineer"
}

2.4. Overriding Concrete Members

Every Mixing needs a quality ratio and an algorithm to mix. We can create concrete members for both in the Mixing trait to provide the default functionality:

val qualityRatio: Double = 3.14 
def algorithm: String = "High instrumental quality"

It’s optional to override the concrete members of a trait. For the sake of our example, let’s consider overriding both qualityRatio and algorithm in the Score class:

class Score(var composer: String,
            var engineer: String,
            var orchestra: String,
            var mixer: String,
            override val qualityRatio: Double)
  extends Composition with SoundProduction {

  // Other fields defined previously

  override def algorithm(): String = {
    if (qualityRatio < 3) "Low instrumental quality"
    else super.algorithm
  }
}

2.5. Testing

It’s time to instantiate the Score class and test the different methods:

class ScoreUnitTest {

  @Test
  def givenScore_whenComposeCalled_thenCompositionIsReturned() = {
    val composer = "Hans Zimmer"
    val engineer = "Matt Dunkley"
    val orchestra = "Berlin Philharmonic"
    val mixer = "Dave Stewart"
    val studio = "Abbey Studios"
    val score = new Score(composer, engineer, orchestra, mixer, 10, studio)

    assertEquals(score.compose(),
      s"""The score is composed by $composer,
        |Orchestration by $orchestra,
        |Mixed by $mixer""".stripMargin)
    }

  @Test
  def givenScore_whenProduceCalled_thenSoundProductionIsReturned() = {
    val composer = "Hans Zimmer"
    val engineer = "Matt Dunkley"
    val orchestra = "Berlin Philharmonic"
    val mixer = "Dave Stewart"
    val studio = "Abbey Studios"
    val score = new Score(composer, engineer, orchestra, mixer, 3, studio)

    assertEquals(score.produce(), s"The score is produced by $engineer")
  }

  @Test
  def givenScore_whenLowQualityRatioSet_thenCorrectAlgorithmIsReturned() = {
    val composer = "Hans Zimmer"
    val engineer = "Matt Dunkley"
    val orchestra = "Berlin Philharmonic"
    val mixer = "Dave Stewart"
    val studio = "Abbey Studios"
    val score = new Score(composer, engineer, orchestra, mixer, 1, studio)

    assertEquals(score.algorithm(), "Low instrumental quality")
  }

  @Test
  def givenScore_whenHighQualityRatioSet_thenCorrectAlgorithmIsReturned() = {
    val composer = "Hans Zimmer"
    val engineer = "Matt Dunkley"
    val orchestra = "Berlin Philharmonic"
    val mixer = "Dave Stewart"
    val studio = "Abbey Studios"
    val score = new Score(composer, engineer, orchestra, mixer, 10, studio)

    assertEquals(score.algorithm(), "High instrumental quality")
  }
}

2.6. Adding a Trait to an Object Instance

Sometimes a particular Score also needs Vocals, in addition to the above members. But the catch here is that not all Score instances would need the same.

Let’s solve for it by first creating a new trait for Vocals:

trait Vocals {
  val sing: String = "Vocals mixin"
}

Now, we need to make it easy for a few Score instances to inherit the above trait. To do so, Scala provides a way to attach a trait directly to an object instance:

// Initialize the other fields
val score = new Score(composer, engineer, orchestra, mixer, 10) with Vocals

assertEquals(score.sing, "Vocals mixin")

2.7. Limiting the Classes That Inherit a Trait

Let’s consider another restriction where a Score can have a SoundProduction only if it is funded by a record label. For that, we create a new RecordLabel type:

class RecordLabel

We need to ensure that SoundProduction can be extended by types if and only if they also extend the RecordLabel type.

One way to do this is to make the SoundProduction trait extend RecordLabel:

trait SoundProduction extends RecordLabel { 
  // Other methods previously defined 
}

Extending the RecordLabel type in the Score class as well we see that there is no compilation error:

class Score(var composer: String,
            var engineer: String,
            var orchestra: String,
            var mixer: String,
            override val qualityRatio: Double,
            var studio: String)
  extends RecordLabel with Composition with SoundProduction { 

  // Other methods previously defined
}

A trait extending a class is not common, so a more elegant way is to set the limiting type to the this property in the SoundProduction trait:

trait SoundProduction {
  this: RecordLabel =>
  
  // Other methods previously defined
}

3. Resolution of Multiple Inheritance Conflicts

In any Score, Composition and SoundProduction can be done in separate recording studios. Let’s add a method called getStudio to the Composition trait:

var studio: String
def getStudio(): String = s"Composed at studio $studio"

And then to SoundProduction:

var studio: String
def getStudio(): String = s"Produced at studio $studio"

Let’s override the above method in the Score class:

class Score(var composer: String,
            var engineer: String,
            var orchestra: String,
            var mixer: String,
            override val qualityRatio: Double,
            var studio: String)
  extends RecordLabel with Composition with SoundProduction {

  // Other methods previously defined

  override def getStudio(): String = super.getStudio()
}

Since we have the same method signature in both the parent traits, there is a conflict when we call super.getStudio() in the Score class. Let’s see how Scala resolves the conflict automatically and how we can force the resolution ourselves.

3.1. Default Conflict Resolution

By default, Scala searches for methods in the parent traits using a right-first and depth-first search.

Since the Score class extends the Composition trait first and then the SoundProduction trait, getStudio() calls the corresponding method in the SoundProduction trait. We can verify the same in a unit test:

// Initialize the other fields
val studio = "Abbey Studios"
val score = new Score(composer, engineer, orchestra, mixer, 10, studio)

assertEquals(score.getStudio(), s"Produced at studio $studio")

3.2. Explicit Conflict Resolution

If we want to explicitly call a conflicting method in the parent traits, then the super keyword can be given a type:

override def getStudio(): String =
  super[Composition].getStudio() + ", " + super[SoundProduction].getStudio()

Let’s see the above in action in our unit test:

assertEquals(
  score.getStudio(),
  s"Composed at studio $studio, Produced at studio $studio"

3.3. Comparison with Java 8 Interfaces

As of Scala 2.12, a trait gets compiled to a single interface class file. This is possible because of Java 8 support for concrete methods (also called default methods) in interfaces.

There is, however, a major difference between a trait and an interface: there’s no automatic conflict resolution of the default methods in Java. So, when we have conflicting methods in the parent interfaces, the compiler expects us to resolve the conflicts using the super keyword.

4. Sealed Traits

Finally, let’s make the algorithm in the Mixing trait an enumeration. Let’s create a sealed trait to represent the MixingAlgorithm:

sealed trait MixingAlgorithm

In the same file, let’s create case objects by extending the sealed trait:

case object LowInstrumentalQuality extends MixingAlgorithm {
  override def toString(): String = "Low instrumental quality"
}

case object HighInstrumentalQuality extends MixingAlgorithm {
  override def toString(): String = "High instrumental quality"
}

Let’s modify the algorithm() in the Mixing trait to return the new enumeration type:

def algorithm: MixingAlgorithm = HighInstrumentalQuality

Let’s change the overridden method in the Score class to return the new type:

override def algorithm(): MixingAlgorithm = {
  if (qualityRatio < 3) LowInstrumentalQuality
  else super.algorithm
}

Since we have overridden the toString in the case objects, the tests can use the same to assert the value:

assertEquals(score.algorithm().toString, "High instrumental quality")

Please note that sealed traits have a couple of important properties:

  • A sealed trait can be extended only in the same file as its declaration
  • Since the compiler knows about all the possible subtypes of a sealed trait, it can perform an exhaustiveness check and throw warnings when we miss a case match

5. Comparison with Abstract Classes

Both traits and abstract classes provide mechanisms for reusability. However, there are a couple of fundamental differences between the two:

  • We can extend from multiple traits, but only one abstract class
  • Abstract classes can have constructor parameters, whereas traits cannot

6. Conclusion

In this tutorial, we saw how to create and extend traits and how they differ from abstract classes.

As always, the full source code for the examples is available over on GitHub.

Comments are closed on this article!