10a. Traits - RobertMakyla/scalaWiki GitHub Wiki
KEY POINTS:
- A class can implement any number of traits.
- Traits can require that implementing classes 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.
Trait instead of interface
Trait can have abstract and concrete methods
A class can implement multiple traits
trait Logger {
type T
def log(msg: T) // abstract method (no body provided)
}
class ConsoleLogger extends Logger { // a class 'extends' trait, not 'implements'
type T = String
def log(msg: T) { println(msg) } // No 'override' needed cause super-method is abstract
}
Class may extend multiple traits / interfaces
All Java interfaces can be used as scala traits. We use 'with' keyword:
class ConsoleLogger extends Logger with Cloneable with Serializable { // 'with' instead of ','
type T = String
def log(msg: T) { println(msg) }
}
As in Java, a Scala class can have only one superclass but any number of traits.
Traits with concrete implementations
trait MyTraitLogger {
def log(msg: String) { println(msg) }
}
class MyClass extends MyTraitLogger { // mixed class with trait's concrete stuff
// can use log()
}
or
class MyClass extends MySuperClass with MyTraitLogger { // mixed class with trait's concrete stuff
// can use log()
}
Objects with Traits ( a bit similar to Stackable Trait Pattern )
trait Logger { // Logger trait
def log(msg: String) { } // doing nothing
}
trait ConsoleLogger extends Logger { // extension of Logger trait
override def log(msg: String) { println(" console: " + msg) }
}
trait FileLogger extends Logger { // extension of Logger trait
override def log(msg: String) { println(" file: " + msg) }
}
class LoggerClient { } // client
Now while creating an object I can decide what trait to 'mix-in' :
val client1 = new LoggerClient with ConsoleLogger
val client2 = new LoggerClient with FileLogger
If I want to mix in a trait - it must have only concrete stuff, nothing abstract.
Layered Traits
when I call client3.log(), all the 3 logs are executed in the right order:
val client3 = new LoggerClient with FirstLogger with SecondLogger with ThirdLogger
Traits for Rich Interfaces
trait Logger { // combination of abstract and concrete methods
def log(msg: String) // abstract method
def info(msg: String) { log("INFO: " + msg) } // concrete methods use abstract method
def warn(msg: String) { log("WARN: " + msg) }
def severe(msg: String) { log("SEVERE: " + msg) }
}
abstract class Account { def logSavings(amount:Int)}
class SavingsAccount extends Account with Logger {
def logSavings(amount: Int) { // I can use any of log/info/warn/severe,
warn("amount = "+ amount) // because log is concrete
}
def log(msg:String) { println(msg) } // must have concrete log method called in trait
}
Concrete Fields in Traits (Added to Mixed in class - not Inherited)
trait ShortLogger {
val maxLength = 15 // A concrete field
}
Each class that will be mixed in this trait, will get this property.
This field is not inherited, it's simply added to subclass, next to all fields from SavingAccount:
class SavingAccount {}
val acct = new SavingAccount with ShortLogger
So, acct will have all properties from SavingAccount + inherited from superclass + added props from ShortLogger
Abstract Fields in Traits (must be supplied)
trait ShortLogger {
val maxLength: Int // abstract field
/* maxLength used in some methods */
}
Declaring a class, mixed-in a trait with abstract field:
class SavingsAccount extends ShortLogger {
val maxLength = 20 // must supply abstract field, no 'override' needed
}
Creating an object, mixed-in a trait with abstract field:
class SavingsAccount { }
val acct = new SavingsAccount with ShortLogger {
val maxLength = 20 // must supply abstract field, no 'override' needed
}
Trait Construction Order
Traits have constructors, executed when constructing any object mixed-in a trait
Traits cannot have constructor parameters. Every trait has a single parameterless constructor:
trait FileLogger extends Logger {
val out = new PrintWriter("app.log") // Part of the trait's constructor
out.println("# " + new Date().toString) // Part of the trait's constructor
def log(msg: String) {
out.println(msg); out.flush()
}
}
Constructor order:
1. The SUPER-class constructor
2. Traits constructors - after the superclass constructor but before the class constructor.
Traits are constructed left-to-right.
Within each trait, the parent trait get constructed first.
If multiple traits share a common parent, and that parent has already been constructed,
it is not constructed again.
3. The SUB-class constructor
E.g.: class SavingsAccount extends Account with FileLogger with ShortLogger
1. Account (the superclass).
2. Logger (the parent of the first trait).
3. FileLogger (the first trait).
4. ShortLogger (the second trait). Note that its parent has already been constructed.
5. SavingsAccount (the class).
Traits vs Classes
1. Traits cannot have constructor parameters
val acct = new SavingsAccount with FileLogger("myapp.log") // ERROR !!!
2. Traits cannot be instantiated. It's always :
val obj = new MyClass with MyTrait
Initializing Trait Fields
trait FileLogger extends Logger {
val filename: String // abstract field
val out = new PrintStream(filename)
def log(msg: String) { out.println(msg); out.flush() }
}
val acct = new SavingsAccount with FileLogger {
val filename = "myapp.log" // Does not work. The problem is construction order
}
The 'new' keyword constructs an anonymous subclass which extends superclass SavingsAccount with FileLogger trait
So the initialisation of 'fileName' field happens in subclass constructor.
It's after trait's constructor calls PrintStream(filename) --> Null Pointer exception
Caution : I cannot use trait's abstract fields already in trait's constructor,
because these fields are instantiated after trait's constructor is called = in subclass constructor
Correct approach 1 (early definition block when instantiating object):
val acct = new { // Early definition block after new
val filename = "myapp.log"
} with SavingsAccount with FileLogger
Correct approach 2 (early definition block in class definition):
class SavingsAccount extends { // Early definition block after extends
val filename = "savings.log"
} with Account with FileLogger {
... // SavingsAccount implementation
}
Correct approach 3 (inefficient but works: lazy value):
lazy val out = new PrintStream(filename) // out will be initialized when used for the first time.
Example: trait MyT {println("MyTrait construction")}
class MySub extends MyC {println("MySubClass construction")}
class MySubComplet extends MyC with MyT {println("MySubComplet construction")}
class MyC {println("MyClass construction")}
class MyE {println("MyEarlyField construction")}
class MyClassWithEarlyBlock extends {val e = new MyE} with MyT {
println("MyClassWithEarlyBlock construction")
}
e.g.1: val a = new MyC with MyT // the same as: new MyC with MyT {}
MyClass construction
MyTrait construction
e.g.2: val a = new MyC with MyT { println("Anonymous subclass construction") }
MyClass construction
MyTrait construction
Anonymous subclass construction
e.g.1a: val a = new MySub with MyT // the same as: new MySub with MyT {}
MyClass construction
MySubClass construction
MyTrait construction
e.g.1b: val a = new MySubComplet // 1.superclass, 2.trait, 3.subclass
MyClass construction
MyTrait construction
MySubComplet construction
e.g.3: val a = new { val e = new MyE } with MyC with MyT
MyEarlyField construction
MyClass construction
MyTrait construction
e.g.3: val a = new { val e = new MyE } with MySubComplet
MyEarlyField construction
MyClass construction
MyTrait construction
MySubComplet construction
e.g.4: val a = new MyClassWithEarlyBlock // 1.superclass, 2.trait, 3.subclass
MyEarlyField construction
MyTrait construction
MyClassWithEarlyBlock construction
Trait Extending Classes
Traits can extend other traits (as above) but they can extend also classes.
Such class becomes superclass of anything that mixes in the trait.
trait Logged extends Exception {
def log() { println( getMessage()) } // uses Exception.getMessage()
}
class UnhappyException extends Logged { // class mixes in a trait
override def getMessage() = "arggh!" // superclass of trait becomes superclass of mixed in class
}
Question : When a subclass (UnhappyException) extends another class (IOException) ?
Answer : It's OK if it (IOException) would be a subclass of trait's superclass (Exception)
class UnhappyException extends IOException with Logged // OK
class UnhappyFrame extends JFrame with Logged // ERROR: JFrame is not a subclass of Exception
Trait extending a class, puts restriction on each class that mixes in the trait.
The restriction is following: each class that mixes-in the trait,
must be subclass of the class that trait is extending
Self Types - BETTER Alternate mechanism to Trait Extending Classes
trait Logged {
this: Exception => // any class mixed in this trait, must be subclass of Exception
def log() { println( getMessage()) }
}
Here, trait does not extend Exception.
But each class mixed in trait, must extend Exception, so will have all the methods from Exception.
val f = nwe JFrame with Logged // ERROR: JFrame is not a subclass of Exception
Trait extending a class is almost the same as Trait with Self Type
1. Self Types can handle circular dependencies between traits.
This can happen if you have two traits that need each other.
2. Self types can also handle Structural Types (types that specify methods without naming a class), e.g.:
trait Logged { // trait can be mixed in anything that has customPrint(s:String) method
this: { def customPrint(s:String) : Unit } =>
def log(s:String) { customPrint(s) }
}
class Mixing { def customPrint(s:String) {println(s)} }
val c = new Mixing with Logged
*Under the hood (Scala on JVM)
trait Logger { // (trait ConsoleLogger extends Logger)
def log (msg:String)
val maxLength = 15 // A concrete field
}
becomes:
public interface Logger { // ( public interface ConsoleLogger extends Logger)
void log(String msg);
public abstract int maxLength();
public abstract void weird_prefix$maxLength_$eq(int); //weird setter for initializing the concrete field
}
To Trait or not to Trait (by M. Odersky)
Whenever you implement a reusable collection of behavior, you will have to decide whether you want to use a trait or an abstract class. There is no firm rule, but this section contains a few guidelines to consider.
If the behavior will not be reused, then make it a concrete class. It is not reusable behavior after all.
If it might be reused in multiple, unrelated classes, make it a trait. Only traits can be mixed into different parts of the class hierarchy.
If you want to inherit from it in Java code, use an abstract class. Since traits with code do not have a close Java analog, it tends to be awkward to inherit from a trait in a Java class. Inheriting from a Scala class, meanwhile, is exactly like inheriting from a Java class. As one exception, a Scala trait with only abstract members translates directly to a Java interface, so you should feel free to define such traits even if you expect Java code to inherit from it.
If you plan to distribute it in compiled form, and you expect outside groups to write classes inheriting from it, you want to use an abstract class. The issue is that when a trait gains or loses a member, any classes that inherit from it must be recompiled, even if they have not changed. If outside clients will only call into the behavior, instead of inheriting from it, then using a trait is fine.
If you still do not know, after considering the above, then start by making it as a trait. You can always change it later, and in general using a trait keeps more options open.