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 S**oundProduction 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.
5.1. Multiple Inheritance
Firstly, we can extend a class from multiple traits but from only one abstract class. Let’s see this in action by defining a class MultipleInheritance that extends from two traits, Trait1 and Trait2:
trait Trait1 {
def method1(): String
}
trait Trait2 {
def method2(): String
}
class MultipleInheritance extends Trait1 with Trait2 {
override def method1(): String = "Trait1 method"
override def method2(): String = "Trait2 method"
}
Further, we can verify that an instance of the MultipleInheritance class has access to methods from both traits:
"A class extended from multiple traits" should "have access to all the methods" in {
val instance = new MultipleInheritance()
instance.method1() shouldEqual "Trait1 method"
instance.method2() shouldEqual "Trait2 method"
}
5.2. Constructor Parameters
Another key difference is that while abstract classes can have constructor parameters, traits in Scala (version<3) don’t support this.
Although Scala 3 removes this differentiating feature between abstract classes and traits, it’s worthwhile learning about trait parameters. So, let’s define the Writer trait for this purpose:
trait Writer(val name: String) {
def introduce = s"Hello, I'm $name"
def write(): String
}
We must note that name* is a trait parameter of type *String. Further, if we try to define trait parameters in Scala 2, it’ll give a compilation error.
Next, let’s add two implementations of the Writer trait, namely, Author and Poet:
class Author(name: String) extends Writer(name) {
def write(): String = s"$name is writing a book"
}
class Poet(name: String) extends Writer(name) {
def write(): String = s"$name is composing poetry"
}
Lastly, let’s verify that the trait parameter name is passed down correctly to an instance of these two classes:
"An Author" should "return a proper introduction" in {
val author = new Author("Mark Twain")
val introduction = author.introduce
introduction shouldEqual "Hello, I'm Mark Twain"
}
"A Poet" should "return a proper introduction" in {
val poet = new Poet("Sylvia Plath")
val introduction = poet.introduce
introduction shouldEqual "Hello, I'm Sylvia Plath"
}
Great! The results are as expected.
6. Conclusion
In this article, 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.