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.