Thursday, September 3, 2009

Adding methods using Traits

One useful application of a trait is the case where you want to add functionality to an existing class. In this example I have a class provided by a third party library (in this just a simple StringReader class from the Java library). But I want to be able to read lines as well as use the standard read methods.

One solution is to create a trait and when I instantiate the StringReader mix in the new trait. Code like new StringReader() with Lines results in a new class that extends StringReader and the trait Lines. As a result we have all the methods of StringReader and Lines. The biggest benefit is that we can define the trait to work with any Reader and then when we create the real instance we can mix it in to any class that extends Reader.

The other solution that can be used is to create an implicit conversion from StringReader to some other class. There are two draw backs here:
  • It is harder to tell what is happening
  • A trait can contain state but a "view" (which is what you get when one class is implicitly converted to another class) has no state it is just a view of the original class. In this example a view would work but in other examples like creating a pushback reader, it would not work.

Here is a simple example:

  1. scala>trait Lines {
  2.      | // the self type declares what type of class the trait can be applied to
  3.      | // if there is no self type then it is assumed it can be applied to Any type
  4.      | self:java.io.Reader =>
  5.      | def nextLine:Option[String] = {
  6.      | val builder = new scala.collection.mutable.StringBuilder()
  7.      |
  8.      | var next = read()
  9.      |
  10.      | while( next != -1 && next.toByte.toChar != '\n' ){
  11.      | builder += next.toByte.toChar
  12.      | next = read()
  13.      | }
  14.      |
  15.      | if( builder.isEmpty ) None
  16.      | else Some(builder.toString)
  17.      | }
  18.      | }
  19. defined trait Lines
  20. // Strings starting and ending with (""") are called raw strings.  All characters 
  21. // within """ """ are automatically escaped.
  22. // I am creating a reader and mixing in the Lines trait on construction
  23. scala>val reader = new java.io.StringReader( """line one
  24.      | line two"""with Lines
  25. reader: java.io.StringReader with Lines = $anon$1@3850620f
  26. scala> reader.nextLine
  27. res0: Option[String] = Some(line one)
  28. scala> reader.nextLine
  29. res1: Option[String] = Some(line two)
  30. scala> reader.nextLine
  31. res2: Option[String] = None
  32. scala> reader.nextLine
  33. res3: Option[String] = None
  34. // we can define a method that takes a reader with lines
  35. scala>def toCollection( reader:java.io.StringReader with Lines) = {
  36.      | def collect:List[String] = reader.nextLine match {
  37.      |   case None => Nil
  38.      |    // we do not need to worry about stack overflow
  39.      |    // because of tail recursion.  This method cannot be
  40.      |    // extended and collect is the last like in the collect
  41.      |    // method so this method will be transformed into a loop
  42.      |   case Some( line ) => line :: collect
  43.      | }
  44.      |
  45.      | collect
  46.      | }
  47. toCollection: (reader: java.io.StringReader with Lines)List[String]
  48. scala> toCollection( new java.io.StringReader( "line one\nlinetwo" ) with Lines).size
  49. res8: Int = 2

No comments:

Post a Comment