Monday, May 3, 2010

Abstract Types vs Parameter

This topic (and the next) are intended to discuss abstract types. A class/trait with an abstract type is quite similar to a class/trait type parameter. For example:

  1. trait C[A] {
  2.   def get : A
  3.   def doit(a:A):A
  4. }
  5. trait C2 {
  6.   type A
  7.   def get : A
  8.   def doit(a:A):A
  9. }

Both implementations have similar properties. However they are NOT the same. At first I thought that I could used them inter-changeably. However, consider the following examples:

  1. //compiles
  2. def p(c:C[Int]) = c.doit(c.get)
  3. // doesn't compile
  4. def p2(c:C2) = c.doit(c.get)

So why doesn't p2 compile? Because it returns A. From the signature of p2 it is impossible to know what p2 returns. There are several ways to fix this problem. One make the method return Unit:

  1. // compiles because the internals of C2 does not leak out
  2. def p(c:C2):Unit = c.doit(c.get)

Another fix would be to change doit to return Unit or an explicit return value like Int

  1. trait C2 {
  2.   type A
  3.   def get : A
  4.   def doit(a:A):Int
  5. }
  6. // compiles correctly
  7. def p(c:C2) = c.doit(c.get)

A second difference between parameterized types and types with abstract type values is illustrated below:

  1. trait C2 {
  2.    type A
  3.    def get : A
  4. }
  5. scala> var c : C2 = new C2 { 
  6.      | type A = Int
  7.      | def get = 3
  8.      | }
  9. c: C2 = $anon$1@11a40fff
  10. // what is the type of result if at compile time the
  11. // value of c is not known
  12. scala> var result = c.get
  13. result: C2#A = 3
  14. scala> c = new C2 {      
  15.      |    type A = String
  16.      |    def get = "hi"
  17.      | }
  18. c: C2 = $anon$1@5f154718
  19. // crazy eh :) the variable can be anything but does not
  20. // have type Any so you cannot assign arbitrary values
  21. scala> result = c.get
  22. result: C2#A = hi
  23. scala> result.isInstanceOf[String]
  24. res0: Boolean = true
  25. // while the dynamic type of result is a string the
  26. // static type is not so you cannot assign a string to result
  27. scala> result = "4"
  28. < console> :8: error: type mismatch;
  29.  found   : java.lang.String("4")
  30.  required: C2#A
  31.        result = "4"
  32.                 ^

The obvious question is what use are abstract types. I don't claim to know them all but the main point is that they do not expose the internal implementation details to the world. The famous cake pattern is one such example usage of abstract types.

I read the following as well (wish I could remember where):

Abstract types are good when extending and there will be concrete subclasses. Param type good for when a type is useful without extension but can handle several types.

A simpler example is examined here. It is loosely based on a real world usecase.
The example below is contrived so that it is smaller than the actual usecase, so consider the design and not the fact that the example could be easier done with other examples. In the real scenario this design reduced the lines of duplicated code from around 500 to 10.

The example below shows how a Traversable like object can be created from InputStreams and Readers. The important aspect is that the type signature of Foreach does not leak information about the implementation. Users of a Foreach object don't care whether it is backed onto an InputStream or Reader. They just care about the type of object contained.

I am leaving this already long post here. The next post will investigate different ways you can get in trouble trying to implement using abstract types.



  1. import java.io.{InputStream, Reader, ByteArrayInputStream, StringReader}
  2. import java.net.URL
  3. object Foreach {
  4.   def fromStream(s: => InputStream) = new Foreach[Int] {
  5.     type I = InputStream
  6.     def source = new Source {
  7.       def in = s
  8.       def next(_in : InputStream) = _in.read match {
  9.         case -1 => None
  10.         case i => Some(i)
  11.       }
  12.     }
  13.   }
  14.   
  15.   def fromReader(s: => Reader) = new Foreach[Char] {
  16.     type I = Reader
  17.     def source = new Source {
  18.       def in = s
  19.       def next(_in : Reader) = _in.read match {
  20.         case -1 => None
  21.         case i => Some(i.toChar)
  22.       }
  23.     }
  24.   }
  25.   
  26.   
  27.   def fromInputAndFunction[A](s: => InputStream, f: Int => A) = new Foreach[A] {
  28.     type I = InputStream
  29.     def source = new Source {
  30.       def in = s
  31.       def next(_in : InputStream) = _in.read match {
  32.         case -1 => None
  33.         case i => Some(f(i))
  34.       }
  35.     }
  36.   }
  37.   
  38.   
  39. }
  40. trait Foreach[A] {
  41.   type I <: java.io.Closeable
  42.   trait Source {
  43.     def in : I
  44.     def next(in : I) : Option[A]
  45.   }
  46.   def source : Source
  47.   
  48.   def foreach[U](f : A => U) : Unit = {
  49.     val s = source.in
  50.     try {
  51.       def processNext : Unit = source.next(s) match {
  52.         case None => 
  53.           ()
  54.         case Some(value) => 
  55.           f(value)
  56.           processNext
  57.       }
  58.       
  59.       processNext
  60.     } finally {
  61.       // correctly handle exceptions
  62.       s.close
  63.     }
  64.   }
  65. }
  66. object Test {
  67.   def main(args : Array[String]) = {
  68.     val data = "Hello World"
  69.     val bytes = data.toArray.map { _.toByte }
  70.     import Foreach._
  71.     fromStream(new ByteArrayInputStream(bytes)).foreach {a => print(a.toChar)}
  72.     
  73.     println
  74.     fromReader(new StringReader(data)) foreach print
  75.     
  76.     println
  77.     
  78.     fromInputAndFunction(new ByteArrayInputStream(bytes), i => i.toChar) foreach print
  79.     
  80.     println
  81.   }
  82. }

7 comments:

  1. "while the dynamic type of result is a string the dynamic type is not so you cannot assign a string to result"

    Didn't you mean:
    "while the dynamic type of result is a string the *static* type is not so you cannot assign a string to result" ?

    ReplyDelete
  2. Abstract types can also be used to implement recursion of types in some situations where type parameters cannot.

    ReplyDelete
  3. Jesse: perhaps you were thinking of this article? http://www.artima.com/weblogs/viewpost.jsp?thread=270195

    ReplyDelete
  4. I have read that article but in fact I am chronicling an issue I had in the Scala IO project.

    ReplyDelete
  5. Excellent article. This helps somewhat in my understanding of abstract types. Do you recommend any other articles?

    ReplyDelete
    Replies
    1. I wish I could remember the articles I was reading at the time... Just what Google could give me :-)

      Delete