Topics in This Chapter L1
In this chapter, you will learn how to work with traits. A class extends one or more traits in order to take advantage of the services that the traits provide. A trait may require implementing classes to support certain features. However, unlike Java interfaces, Scala traits can supply state and behavior for these features, which makes them far more useful.
Key points of this chapter:
A class can implement any number of traits.
Traits can require implementing classes to have certain fields, methods, or superclasses.
Unlike Java interfaces, a Scala trait can provide implementations of methods and fields.
When you layer multiple traits, the order matters—the trait whose methods execute first goes to the back.
Traits are compiled into Java interfaces. A class implementing traits is compiled into a Java class with all the methods and fields of its traits.
Use a self type declaration to indicate that a trait requires another type.
Scala, like Java, does not allow a class to inherit from multiple superclasses. At first, this seems like an unfortunate restriction. Why shouldn’t a class extend multiple classes? Some programming languages, in particular C++, allow multiple inheritance—but at a surprisingly high cost.
Multiple inheritance works fine when you combine classes that have nothing in common. But if these classes have common methods or fields, thorny issues come up. Here is a typical example. A teaching assistant is a student and also an employee:
class Student :
def id: String = ...
...
class Employee :
def id: String = ...
...
Suppose we could have
class TeachingAssistant extends Student, Employee //
Not actual Scala code
Unfortunately, this TeachingAssistant
class inherits two id
methods. What should myTA.id
return? The student ID? The employee ID? Both? (In C++, you need to redefine the id
method to clarify what you want.)
Next, suppose that both Student
and Employee
extend a common superclass Person
:
class Person :
var name: String = null
class Student extends Person :
...
class Employee extends Person :
...
This leads to the diamond inheritance problem (see Figure 10–1). We only want one name
field inside a TeachingAssistant
, not two. How do the fields get merged? How does the field get constructed? In C++, you use “virtual base classes,” a complex and brittle feature, to address this issue.
Java designers were so concerned about these complexities that they took a very restrictive approach. A class can extend only one superclass; it can implement any number of interfaces, but interfaces can have only abstract, static, or default methods, and no fields.
Java default methods are very limited. They can call other interface methods, but they cannot make use of object state. It is therefore common in Java to provide both an interface and an abstract base class, but that just kicks the can down the road. What if you need to extend two of those abstract base classes?
Scala has traits instead of interfaces. A trait can have abstract and concrete methods, as well as state. In fact, a trait can do everything a class does. There are just three differences between classes and traits:
You cannot instantiate a trait.
In trait methods, calls of the form super.someMethod
are dynamically resolved.
Traits cannot have auxiliary constructors.
You will see in the following sections how Scala deals with the perils of conflicting features from multiple traits.
Let’s start with the simplest case. A Scala trait can work exactly like a Java interface, declaring one or more abstract methods. For example:
trait Logger :
def log(msg: String) : Unit //
An abstract method
Note that you need not declare the method as abstract
—an unimplemented method in a trait is automatically abstract.
A subclass can provide an implementation:
class ConsoleLogger extends Logger : //
Use extends, not implementsdef log(msg: String) = println(msg) //
No override needed
You need not supply the override
keyword when overriding an abstract method of a trait.
Note
Scala doesn’t have a special keyword for implementing a trait. You use the same keyword extends
for forming a subtype of a class or a trait.
If you need more than one trait, add the others using commas:
class FileLogger extends Logger, AutoCloseable, Appendable :
...
Note the AutoCloseable
and Appendable
interfaces from the Java library. All Java interfaces can be used as Scala traits.
As in Java, a Scala class can have only one superclass but any number of traits.
Note
You can use the with
keyword instead of commas:
class FileLogger extends Logger with AutoCloseable with Appendable
In Scala, the methods of a trait need not be abstract. For example, we can make our ConsoleLogger
into a trait:
trait ConsoleLogger extends Logger :
def log(msg: String) = println(msg)
The ConsoleLogger
trait provides a method with an implementation—in this case, one that prints the logging message on the console.
Here is an example of using this trait:
class Account :
protected var balance = 0.0
class ConsoleLoggedAccount extends Account, ConsoleLogger :
def withdraw(amount: Double) =
if amount > balance then log("Insufficient funds")
else balance -= amount
...
Note how the ConsoleLoggedAccount
picks up a concrete implementation from the ConsoleLogger
trait. In Java, this is also possible by using default methods in interfaces.
In Scala (and other programming languages that allow this), we say that the ConsoleLogger
functionality is “mixed in” with the ConsoleLoggedAccount
class.
Note
Supposedly, the “mix in” term comes from the world of ice cream. In the ice cream parlor parlance, a “mix in” is an additive that is kneaded into a scoop of ice cream before dispensing it to the customer—a practice that may be delicious or disgusting depending on your point of view.
A trait can have many utility methods that depend on a few abstract ones. One example is the Scala Iterator
trait that defines dozens of methods in terms of the abstract next
and hasNext
methods.
Let us enrich our rather anemic logging API. Usually, a logging API lets you specify a level for each log message to distinguish informational messages from warnings or errors. We can easily add this capability without forcing any policy for the destination of logging messages.
trait Logger :
def log(msg: String) : Unit
def info(msg: String) = log(s"INFO: $msg")
def warn(msg: String) = log(s"WARN: $msg")
def severe(msg: String) = log(s"SEVERE: $msg")
Note the combination of abstract and concrete methods.
A class that uses the Logger
trait can now call any of these logging messages. For example, this class uses the severe
method:
class ConsoleLoggedAccount extends Account, ConsoleLogger :
def withdraw(amount: Double) =
if amount > balance then severe("Insufficient funds")
else balance -= amount
...
This use of concrete and abstract methods in a trait is very common in Scala. In Java, you can achieve the same with default methods.
You can add a trait to an individual object when you construct it. Let’s first define this class:
abstract class LoggedAccount extends Account, Logger :
def withdraw(amount: Double) =
if amount > balance then log("Insufficient funds")
else balance -= amount
This class is abstract since it can’t yet do any logging, which might seem pointless. But you can “mix in” a concrete logger trait when constructing an object.
Let’s assume the following concrete trait:
trait ConsoleLogger extends Logger :
def log(msg: String) = println(msg)
Here is how you can construct an object:
val acct = new LoggedAccount() with ConsoleLogger
Caution
Note that you need the new
keyword to construct an object that mixes in a trait. (With new
, you don’t need empty parentheses to invoke the no-argument constructor of the class, but I am adding them for consistency.)
You also need to use the with
keyword, not a comma, before each trait.
When calling log
on the acct
object, the log
method of the ConsoleLogger
trait executes.
Of course, another object can add in a different concrete trait:
val acct2 = new LoggedAccount() with FileLogger
You can add, to a class or an object, multiple traits that invoke each other starting with the last one. This is useful when you need to transform a value in stages.
Here is a simple example. We may want to add a timestamp to all logging messages.
trait TimestampLogger extends ConsoleLogger :
override def log(msg: String) =
super.log(s"${java.time.Instant.now()} $msg")
Also, suppose we want to truncate overly chatty log messages like this:
trait ShortLogger extends ConsoleLogger :
override def log(msg: String) =
super.log(
if msg.length <= 15 then msg
else s"${msg.substring(0, 14)}...")
Note that each of the log
methods passes a modified message to super.log
.
With traits, super.log
does not have the same meaning as it does with classes. Instead, super.log
calls the log
method of another trait, which depends on the order in which the traits are added.
To see how the order matters, compare the following two examples:
val acct1 = new LoggedAccount() with TimestampLogger with ShortLogger
val acct2 = new LoggedAccount() with ShortLogger with TimestampLogger
If we overdraw acct1
, we get a message
2021-09-30T10:32:46.309584537Z Insufficient f...
As you can see, the ShortLogger
’s log
method was called first, and its call to super.log
called the TimestampLogger
.
However, overdrawing acct2
yields
2021-09-30T10:...
Here, the TimestampLogger
appeared last in the list of traits. Its log
message was called first, and the result was subsequently shortened.
For simple mixin sequences, the “back to front” rule will give you the right intuition. See Section 10.10, “Trait Construction Order,” on page 138 for the gory details that arise when the traits form a more complex graph.
Note
With traits, you cannot tell from the source code which method is invoked by super.someMethod
. The exact method depends on the ordering of the traits in the object or class that uses them. This makes super
far more flexible than in plain old inheritance.
Note
If you want to control which trait’s method is invoked, you can specify it in brackets: super[ConsoleLogger].log(...)
. The specified type must be an immediate supertype; you can’t access traits or classes that are further away in the inheritance hierarchy.
In the preceding section, the TimestampLogger
and ShortLogger
traits extended ConsoleLogger
. Let’s make them extend our Logger
trait instead, where we provide no implementation to the log
method.
trait Logger :
def log(msg: String) : Unit //
This method is abstract
Then, the TimestampLogger
class no longer compiles.
trait TimestampLogger extends Logger :
override def log(msg: String) = //
Overrides an abstract methodsuper.log(s"${java.time.Instant.now()} $msg") //
Is super.log defined?
The compiler flags the call to super.log
as an error.
Under normal inheritance rules, this call could never be correct—the Logger.log
method has no implementation. But actually, as you saw in the preceding section, there is no way of knowing which log
method is actually being called—it depends on the order in which traits are mixed in.
Scala takes the position that TimestampLogger.log
is still abstract—it requires a concrete log
method to be mixed in. You therefore need to tag the method with the abstract
keyword and the override
keyword, like this:
abstract override def log(msg: String) =
super.log(s"${java.time.Instant.now()} $msg")
A field in a trait can be concrete or abstract. If you supply an initial value, the field is concrete.
trait ShortLogger extends Logger :
val maxLength = 15 // A concrete field
abstract override def log(msg: String) =
super.log(
if msg.length <= maxLength then msg
else s"${msg.substring(0, maxLength - 1)}...")
A class that mixes in this trait acquires a maxLength
field. In general, a class gets a field for each concrete field in one of its traits. These fields are not inherited; they are simply added to the subclass. Let us look at the process more closely, with a SavingsAccount
class that has a field to store the interest rate:
class SavingsAccount extends Account, ConsoleLogger, ShortLogger :
var interest = 0.0
...
The superclass has a field:
class Account :
protected var balance = 0.0
...
A SavingsAccount
object is made up of the fields of its superclasses, together with the fields in the subclass. In Figure 10–2, you can see that the balance
field is contributed by the Account
superclass, and the interest
field by the subclass.
In the JVM, a class can only extend one superclass, so the trait fields can’t be picked up in the same way. Instead, the Scala compiler adds the maxLength
field to the SavingsAccount
class, together with the interest
field.
Caution
When you extend a class and then change the superclass, the subclass doesn’t have to be recompiled because the virtual machine understands inheritance. But when a trait changes, all classes that mix in that trait must be recompiled.
You can think of concrete trait fields as “assembly instructions” for the classes that use the trait. Any such fields become fields of the class.
An uninitialized field in a trait is abstract and must be overridden in a concrete subclass.
For example, the following maxLength
field is abstract:
trait ShortLogger extends Logger :
val maxLength: Int //
An abstract fieldabstract override def log(msg: String) =
super.log(
if msg.length <= maxLength then msg
else s"${msg.substring(0, maxLength - 1)}...")
//
The maxLength field is used in the implementation
When you use this trait in a concrete class, you must supply the maxLength
field:
class ShortLoggedAccount extends LoggedAccount, ConsoleLogger, ShortLogger :
val maxLength = 20 //
No override necessary
Now all logging messages are truncated after 20 characters.
This way of supplying values for trait parameters is particularly handy when you construct objects on the fly. You can truncate the messages in an instance as follows:
val acct = new LoggedAccount() with ConsoleLogger with ShortLogger :
val maxLength = 15
Just like classes, traits can have primary constructors. Let’s defer constructor parameters until the next section. In the absence of parameters, the primary constructor consists of field initializations and other statements in the trait’s body. For example,
trait FileLogger extends Logger :
println("Constructing FileLogger") // Constructor code
private val out = PrintWriter("/tmp/log.txt") // Constructor code
def log(msg: String) =
out.println(msg)
out.flush()
The trait’s primary constructor is executed during construction of any object incorporating the trait.
Constructors execute in the following order:
The superclass constructor is called first.
Trait constructors are executed after the superclass constructor but before the class constructor.
Traits are constructed left-to-right.
Within each trait, the parents get constructed first.
If multiple traits share a common parent, and that parent has already been constructed, it is not constructed again.
After all traits are constructed, the subclass is constructed.
For example, consider this class:
class FileLoggedAccount extends Account, FileLogger, TimestampLogger
The constructors execute in the following order:
Account
(the superclass).
Logger
(the parent of the first trait).
FileLogger
(the first trait).
TimestampLogger
(the second trait). Note that its Logger
parent has already been constructed.
FileLoggedAccount
(the class).
Note
The constructor ordering is the reverse of the linearization of the class. The linearization is a technical specification of all supertypes of a type. It is defined by the rule:
If C extends C1, C2, ... , Cn, then lin(C) =
C » lin(Cn) » ... » lin(C2) » lin(C1)
Here, » means “concatenate and remove duplicates, with the right winning out.” For example,
lin(FileLoggedAccount)
= FileLoggedAccount » lin(TimestampLogger) » lin(FileLogger) »
lin(Account)
= FileLoggedAccount » (TimestampLogger » Logger) »
(FileLogger » Logger) » lin(Account)
= FileLoggedAccount » TimestampLogger » FileLogger » Logger » Account.
(For simplicity, I omitted the types AnyRef
, and Any
that are at the end of any linearization.)
The linearization gives the order in which super
is resolved in a trait. For example, calling super
in a TimestampLogger
invokes the FileLogger
method.
In the preceding section, we looked at trait constructors without parameters. You saw how a given trait is constructed exactly once. Let’s turn to trait constructors with parameters. For a file logger, one would like to specify the log file:
trait FileLogger(filename: String) extends Logger :
private val out = PrintWriter(filename)
def log(msg: String) =
out.println(msg)
out.flush()
Then you pass the file name when mixing in the file logger:
val acct = new LoggedAccount() with FileLogger("/tmp/log.txt")
Of course, it must be guaranteed that the trait is initialized exactly once. To ensure this, there are three simple rules:
A class must initialize any uninitialized trait that it extends.
A class cannot initialize a trait that a superclass already initialized.
A trait cannot initialize another trait.
Let us go through these rules with some examples. First, consider a class extending a parameterized trait. It must provide an argument. For example, the following would be illegal:
class FileLoggedAccount extends LoggedAccount, FileLogger
//
Error—no argument for FileLogger constructor
The remedy is to provide an argument:
class FileLoggedAccount(filename: String) extends LoggedAccount, FileLogger(filename)
You cannot initalize a trait that was already initialized by a superclass. This isn’t a common issue, so here is a contrived example:
class TmpLoggedAccount extends Account, FileLogger("/tmp/log.txt")
class FileLoggedAccount(filename) extends TmpLoggedAccount, FileLogger(filename)
//
Error—FileLogger already initialized
Finally, a trait extending a parameterized trait cannot pass initialization arguments.
trait TimestampFileLogger extends FileLogger("/tmp/log.txt") :
//
Error—a trait cannot call the constructor of another trait
Instead, drop the constructor parameter:
trait TimestampFileLogger extends FileLogger :
override def log(msg: String) = super.log(s"${java.time.Instant.now()} $msg")
The initialization must happen in each class using a TimestampFileLogger
:
val acct2 =
new LoggedAccount() with TimestampFileLogger with FileLogger("/tmp/log.txt")
As you have seen, a trait can extend another trait, and it is common to have a hierarchy of traits. Less commonly, a trait can also extend a class. That class becomes a superclass of any class mixing in the trait.
Here is an example. The LoggedException
trait extends the Exception
class:
trait LoggedException extends Exception, ConsoleLogger :
override def log(msg: String) = super.log(s"${getMessage()} $msg")
A LoggedException
has a log
method to log the exception’s message. Note that the log
method calls the getMessage
method that is inherited from the Exception
superclass.
Now let’s form a class that mixes in this trait:
class UnhappyException extends LoggedException : // This class extends a trait
override def getMessage() = "arggh!"
The superclass of the trait becomes the superclass of our class (see Figure 10–3).
What if our class already extends another class? That’s OK, as long as it’s a subclass of the trait’s superclass. For example,
class UnhappyIOException extends IOException, LoggedException
Here UnhappyIOException
extends IOException
, which already extends Exception
. When mixing in the trait, its superclass is already present, and there is no need to add it.
However, if our class extends an unrelated class, then it is not possible to mix in the trait. For example, you cannot form the following class:
class UnhappyFrame extends javax.swing.JFrame, LoggedException
//
Error: Unrelated superclasses
It would be impossible to add both JFrame
and Exception
as superclasses.
Scala translates traits into interfaces of the JVM. You are not required to know how this is done, but you may find it helpful for understanding how traits work.
A trait that has only abstract methods is simply turned into a Java interface. For example,
trait Logger :
def log(msg: String) : Unit
turns into
public interface Logger { // Generated Java interface
void log(String msg);
}
Trait methods become default methods. For example,
trait ConsoleLogger :
def log(msg: String) = println(msg)
becomes
public interface ConsoleLogger {
default void log(String msg) { ... }
}
If the trait has fields, the Java interface has getter and setter methods.
trait ShortLogger extends ConsoleLogger :
val maxLength = 15 // A concrete field
...
is translated to
public interface ShortLogger extends Logger {
int maxLength();
void some_prefix$maxLength_$eq(int);
default void log(String msg) { ... } // Calls maxLength()
default void $init$() { some_prefix$maxLength_$eq(15); }
}
Of course, the interface can’t have any fields, and the getter and setter methods are unimplemented. The getter is called when the field value is needed.
The setter is needed to initialize the field. This happens in the $init$
method.
When the trait is mixed into a class, the class gets a maxLength
field, and the getter and setter are defined to get and set that field. The constructors of the class invoke the $init$
method of the trait. For example,
class ShortLoggedAccount extends Account, ShortLogger
turns into
public class ShortLoggedAccount extends Account implements ShortLogger {
private int maxLength;
public int maxLength() { return maxLength; }
public void some_prefix$maxLength_$eq(int arg) { maxLength = arg; }
public ShortLoggedAccount() {
super();
ShortLogger.$init$();
}
...
}
If a trait extends a superclass, the trait still turns into an interface. Of course, a class mixing in the trait extends the superclass.
As an example, consider the following trait:
trait LoggedException extends Exception, ConsoleLogger :
override def log(msg: String) = super.log(s"${getMessage()} $msg")
It becomes a Java interface. The superclass is nowhere to be seen.
public interface LoggedException extends ConsoleLogger {
public void log();
}
When the trait is mixed into a class, then the class extends the trait’s superclass. For example,
class UnhappyException extends LoggedException :
override def getMessage() = "arggh!"
becomes
public class UnhappyException extends Exception implements LoggedException
Consider this inheritance hierarchy:
class Person
class Employee extends Person, Serializable, Cloneable
class Contractor extends Person, Serializable, Cloneable
When you declare
val p = if scala.math.random() < 0.5 then Employee() else Contractor()
you probably expect p
to have type Person
. Actually, the type is Person & Cloneable
. That actually makes sense: both Employee
and Contractor
are subtypes of Cloneable
.
Why isn’t the inferred type Person & Serializable & Cloneable
? The Serializable
trait is marked as transparent so that it is not used for type inference. Other transparent traits include Product
and Comparable
.
In the unlikely situation that you want to declare another trait as transparent, here is how to do it:
transparent trait Logged
A trait can require that it is mixed into a class that extends another type. You achieve this with a self type declaration, which has the following unlovable syntax:
this: Type =>
In the following example, the LoggedException
trait can only be mixed into a class that extends Exception
:
trait LoggedException extends Logger :
this: Exception =>
def log(): Unit = log(getMessage())
//
OK to call getMessage because this is an Exception
If you try to mix the trait into a class that doesn’t conform to the self type, an error occurs:
val f = new Account() with LoggedException
//
Error: Account isn’t a subtype of Exception, the self type of LoggedException
A trait with a self type is similar to a trait with a supertype. In both cases, it is ensured that a type is present in a class that mixes in the trait. However, self types can handle circular dependencies between traits. This can happen if you have two traits that need each other.
Caution
Self types do not automatically inherit. If you define
trait MonitoredException extends LoggedException
you get an error that MonitoredException
doesn’t supply Exception
. In this situation, you need to repeat the self type:
trait MonitoredException extends LoggedException :
this: Exception =>
To require multiple types, use an intersection type:
this: T & U & ... =>
Note
If you give a name other than this
to the variable in the self type declaration, then it can be used in subtypes by that name. For example,
trait Group :
outer: Network =>
class Member :
...
Inside Member
, you can refer to the this
reference of Group
as outer
. By itself, that is not an important benefit since you could introduce the name as follows:
trait Group :
val self: this.type = this
class Member :
...
1. The java.awt.Rectangle
class has useful methods translate
and grow
that are unfortunately absent from classes such as java.awt.geom.Ellipse2D
. In Scala, you can fix this problem. Define a trait RectangleLike
with concrete methods translate
and grow
. Provide any abstract methods that you need for the implementation, so that you can mix in the trait like this:
val egg = java.awt.geom.Ellipse2D.Double(5, 10, 20, 30) with RectangleLike
egg.translate(10, -10)
egg.grow(10, 20)
2. Define a class OrderedPoint
by mixing scala.math.Ordered[Point]
into java.awt.Point
. Use lexicographic ordering, i.e. (x, y) < (x’, y’) if x < x’ or x = x’ and y < y’.
3. Look at the BitSet
class, and make a diagram of all its superclasses and traits. Ignore the type parameters (everything inside the [...]
). Then give the linearization of the traits.
4. Provide a CryptoLogger
trait that encrypts the log messages with the Caesar cipher. The key should be 3 by default, but it should be overridable by the user. Provide usage examples with the default key and a key of −3.
5. The JavaBeans specification has the notion of a property change listener, a standardized way for beans to communicate changes in their properties. The PropertyChangeSupport
class is provided as a convenience superclass for any bean that wishes to support property change listeners. Unfortunately, a class that already has another superclass—such as JComponent
—must reimplement the methods. Reimplement PropertyChangeSupport
as a trait, and mix it into the java.awt.Point
class.
6. In the Java AWT library, we have a class Container
, a subclass of Component
that collects multiple components. For example, a Button
is a Component
, but a Panel
is a Container
. That’s the composite pattern at work. Swing has JComponent
and JButton
, but if you look closely, you will notice something strange. JComponent
extends Container
, even though it makes no sense to add other components to, say, a JButton
. Ideally, the Swing designers would have preferred the design in Figure 10–4.
But that’s not possible in Java. Explain why not. How could the design be executed in Scala with traits?
7. Construct an example where a class needs to be recompiled when one of the mixins changes. Start with class ConsoleLoggedAccount extends Account, ConsoleLogger
. Put each class and trait in a separate source file. Add a field to Account
. In your main method (also in a separate source file), construct a ConsoleLoggedAccount
and access the new field. Recompile all files except for ConsoleLoggedAccount
and verify that the program works. Now add a field to ConsoleLogger
and access it in your main method. Again, recompile all files except for ConsoleLoggedAccount
. What happens? Why?
8. There are dozens of Scala trait tutorials with silly examples of barking dogs or philosophizing frogs. Reading through contrived hierarchies can be tedious and not very helpful, but designing your own is very illuminating. Make your own silly trait hierarchy example that demonstrates layered traits, concrete and abstract methods, concrete and abstract fields, and trait parameters.
9. In the java.io
library, you add buffering to an input stream with a BufferedInputStream
decorator. Reimplement buffering as a trait. For simplicity, override the read
method.
10. Using the logger traits from this chapter, add logging to the solution of the preceding problem that demonstrates buffering.
11. Implement a class IterableInputStream
that extends java.io.InputStream
with the trait Iterable[Byte]
.
12. Using javap -c -private
, analyze how the call super.log(msg)
is translated to Java byte codes. How does the same call invoke two different methods, depending on the mixin order?
13. Consider this trait that models a physical dimension:
trait Dim[T](val value: Double, val name: String) :
protected def create(v: Double): T
def +(other: Dim[T]) = create(value + other.value)
override def toString() = s"$value $name"
Here is a concrete subclass:
class Seconds(v: Double) extends Dim[Seconds](v, "s") :
override def create(v: Double) = new Seconds(v)
But now a knucklehead could define
class Meters(v: Double) extends Dim[Seconds](v, "m") :
override def create(v: Double) = new Seconds(v)
allowing meters and seconds to be added. Use a self type to prevent that.
14. Look for an example using Scala self types on the web. Can you eliminate the self type by extending from a supertype, like in the LoggingException
example in Section 10.15, “Self Types,” on page 143? If the self type is actually required, is it used to break circular dependencies?