Chapter 10. Traits

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.

10.1 Why No Multiple Inheritance?

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.

Images

Figure 10–1 Diamond inheritance must merge common fields.

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.

10.2 Traits as Interfaces

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 implements
  def log(msg: String) = println(msg) // No override needed

You need not supply the override keyword when overriding an abstract method of a trait.

Images 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.

Images Note

You can use the with keyword instead of commas:

class FileLogger extends Logger with AutoCloseable with Appendable

10.3 Traits with Concrete Methods

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.

Images 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.

10.4 Traits for Rich Interfaces

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.

10.5 Objects with Traits

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

Images 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

10.6 Layered Traits

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.

Images 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.

Images 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.

10.7 Overriding Abstract Methods in Traits

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 method
    super.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")

10.8 Concrete Fields in Traits

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.

Images

Figure 10–2 Fields from a trait are placed in 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.

Images 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.

10.9 Abstract Fields in Traits

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 field
  abstract 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

10.10 Trait Construction Order

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:

  1. The superclass constructor is called first.

  2. Trait constructors are executed after the superclass constructor but before the class constructor.

  3. Traits are constructed left-to-right.

  4. Within each trait, the parents get constructed first.

  5. If multiple traits share a common parent, and that parent has already been constructed, it is not constructed again.

  6. 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:

  1. Account (the superclass).

  2. Logger (the parent of the first trait).

  3. FileLogger (the first trait).

  4. TimestampLogger (the second trait). Note that its Logger parent has already been constructed.

  5. FileLoggedAccount (the class).

Images 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.

10.11 Trait Constructors with Parameters

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:

  1. A class must initialize any uninitialized trait that it extends.

  2. A class cannot initialize a trait that a superclass already initialized.

  3. 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")

10.12 Traits Extending Classes

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).

Images

Figure 10–3 The superclass of a trait becomes the superclass of any class mixing in the trait.

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.

10.13 What Happens under the Hood

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

10.14 Transparent Traits L2

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

10.15 Self Types L2

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.

Images 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 & ... =>

Images 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 :
    ...

Exercises

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.

Images

Figure 10–4 A better design for Swing containers

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?

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.15.137.91