Showing posts with label self-type. Show all posts
Showing posts with label self-type. Show all posts

Monday, February 15, 2010

Self Annotation vs inheritance

At first glance the "sself annotation" declaration seems similar to extending another class. (For a look at self annotations read the topic: Self Type.) They are completely different but the comparison is understandable since both of them provide access to the functionality of referenced class.

For example both of the following compile:
  1. class Base {
  2.   def magic = "bibbity bobbity boo!!"
  3. }
  4. trait Extender extends Base {
  5.   def myMethod = "I can "+magic
  6. }
  7. trait SelfTyper {
  8.   self : Base => 
  9.   
  10.   def myMethod = "I can "+magic
  11. }

But the two are completely different. Extender can be mixed in with any class and adds both the "magic" and "myMethod" to the class it is mixed with. SelfType can only be mixed in with a class that extends Base and SelfTyper only adds the method "myMethod" NOT "magic".

Why is the "self annotations" useful? Because it allows several provides a way of declaring dependencies. One can think of the self annotation declaration as the phrase "I am useable with" or "I require a".

The following example demonstrates one possible reason to use self annotations instead of extend.

Note: These examples can be pasted into the REPL but I have shown that here because it would make the examples too long.
  1. import java.io._
  2. import java.util.{Properties => JProps}
  3. trait Properties {
  4.   def apply(key:String) : String
  5. }
  6. trait XmlProperties extends Properties {
  7.   import scala.xml._
  8.   
  9.   def xml(key:String) = Elem(null,key,Null,TopScope, Text(apply(key)))
  10. }
  11. trait JSonProperties extends Properties {
  12.   def json(key:String) : String = "%s : %s".format(key, apply(key))
  13. }
  14. trait StreamProperties {
  15.   self : Properties => 
  16.   
  17.   protected def source : InputStream
  18.   
  19.   private val props = new JProps()
  20.   props.load(source)
  21.   
  22.   def apply(key:String) = props.get(key).asInstanceOf[String]
  23. }
  24. trait MapProperties {
  25.   self : Properties =>
  26.   
  27.   protected def source : Map[String,String]
  28.   def apply(key:String) = source.apply(key)
  29. }
  30. val sampleMap = Map("1" -> "one""2" -> "two""3" -> "three")
  31. val sampleData = """1=one
  32.                     2=two
  33.                     3=three"""
  34. val sXml = new XmlProperties() with StreamProperties{
  35.               def source = new ByteArrayInputStream(sampleData.getBytes)
  36.            }
  37. val mXml = new XmlProperties() with MapProperties{
  38.               def source = sampleMap
  39.            }
  40. val sJSon = new JSonProperties() with StreamProperties{
  41.               def source = new ByteArrayInputStream(sampleData.getBytes)
  42.             }
  43. val mJSon = new JSonProperties() with MapProperties{
  44.               def source = sampleMap
  45.             }
  46. sXml.xml("1")
  47. mXml.xml("2")
  48. sJSon.json("1")
  49. mJSon.json("2")

The justification for using self annotations here is flexibility. A couple other solutions would be
  1. Use subclassing - this is poor solution because there would be an explosion of classes. Instead of having 5 traits you would need 7 traits. Properties, XmlProperties, JSonProperties, XmlStreamProperties, XmlMapProperties, JsonStreamProperties and JsonMapProperties. And if you later wanted to add a new type of properties or a new source like reading from a database then you need 2 new subclasses.
  2. Composition - Another strategy is to use construct the XmlProperties with a strategy that reads from the source. This is essentially the same mechanism except that you need to build and maintain the the dependencies. It also makes layering more difficult. For example:
    1. trait IterableXmlProperties {                                            
    2.     self : MapProperties with XmlProperties => 
    3.     def xmlIterable = source.keySet map {xml _}
    4.   }
    5.   
    6.   new XmlProperties with MapProperties with IterableXmlProperties {def source = sampleMap}

The next question that comes to mind is why use extends then if self annotation is so flexible? My answer (and I welcome discussion on this point) has three points.
  1. The first is of semantics and modeling. When designing a model it is often more logical to use inheritance because of the semantics that comes with inheriting from another object.
  2. Another argument is pragmatism.
    Imagine the collections library where there is no inheritance. If you wanted a map with Iterable functionality you would have to always declare Traversable with Iterable with Map (and this is greatly simplified). That declaration would have to be used for virtually all methods that require both the Iterable and Map functionality. To say that is impractical is an understatement. Also the semantics of Map is changed from what it is now. The trait Map currently includes the concept of Iterable.
  3. The last point is sealed traits/classes. When a trait is "sealed" all of its subclasses are declared within the same file and that makes the set of subclasses finite which allows certain compiler checks. This (as far as I know) cannot be done with self annotations.

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