2015-01-08

Introduce Type Param Pattern

I've always had the feeling that many of the common design patterns were simply recipes for doing something that your programming language does not do for you. The standard Gang of Four design patterns apply to many different programming languages, because these are things that most object oriented programming languages, at least at the time, weren't prepared to help you with. Just which design patterns are relevant is something that changes over time, as languages evolve to do more for you. A classic example is the singleton pattern and Scala. Scala provides singleton objects as a language feature, and most of the implementation concerns addressed by the singleton pattern are now handled by the language.

On the other hand, as languages get more powerful, we expect more from them, and are naturally inclined to attempt to do more sophisticated things than we used to. I often get myself in trouble with Scala, because I try to do things I would never attempt to do in Java. In a position like this, we may need to consider new patterns to get around the newly discovered limitations of the new language. I "invented" a little Scala-specific pattern that I will show here, that I've used to help me overcome a small problem I come across from time to time.

When I do get in trouble with Scala, it's generally because I'm trying to get sophisticated with the type system. Scala's type system is so powerful, it's very alluring to try to strongly type everything. But while it doesn't always work out the way I imagined, sometimes there are workarounds that are easy enough. One workaround that I've found, I call the "introduce type param" pattern. It goes something like this. Let's start with a simple parameterized class, a container of some sort that "holds" an instance of any type:

class Box[E](e: E) {
  def unbox: E = e
  def rebox(e: E): Box[E] = new Box(e)

}

You'll notice that I've cleverly design Box to both take parameters and return values of the parameterized type E. The rebox method is really very uninteresting and useless, but it has the characteristic I need to show out the example: It returns a Box with the same parameterized type E.

Now let's say I have a collection of Boxes of arbitrary type. I just use an empty sequence here, as I am only interested in what compiler thinks I am doing with types:

val boxes: Seq[Box[_]] = Seq()

I would like to iterate through my list of boxes, and unbox and rebox everything. Here is my first attempt:

// this doesn't work:
val newBoxesFail1 = boxes map { box =>
  val e = box.unbox
  val newBox = box.rebox(e)
  newBox
}

To my eye, it looks like this should compile fine. My anonymous function takes a single Box argument, and the type of the instance held by the box is not specified. But we know it is a real type, so let's just call it EE. Now e has type EE, and box has type Box[EE], so you would think that I could safely call box.rebox(e), but the Scala compiler doesn't think so. Instead, I get the following error with the 2.11.4 compiler:

introduce-type-param/introduceTypeParam.scala:13: type mismatch;
found   : e.type (with underlying type Any)
required: _$1
    val newBox = box.rebox(e)
                           ^
one error found

My feeling is that the Scala compiler really should be able to figure this out. I suspect that the Scala compiler team already knows about this. Assuming they do, I suspect they consider it a weakness/limitation of the compiler/language, and that it is somewhere on their priority list to address. I'm not going to look into submitting it as a bug, because I figure that the compiler team is very busy, and what they are already working on is more important than investigating and addressing what would most likely be a duplicate bug report.

And besides all that, I have a workaround. I learned to get around the problem by introducing a type parameter into the code to represent the type EE. In order to do this, I have to extract a type-parameterized method to do the unboxing and reboxing:

def repackage[EE](box: Box[EE]): Box[EE] = {
  val e = box.unbox
  val newBox = box.rebox(e)
  newBox
}

val newBoxesPass = boxes map { b => repackage(b) }

Now the compiler is happy. By introducing the new type parameter, we allow the compiler to see that the type of e is the same as the type of the parameter to box.rebox. It's important to note that the anonymous function { b => repackage(b) } essentially acts as a converter function from type Box[_] to type Box[EE]. For instance, the following does not work:

val newBoxesFail2 = boxes map repackage _

It doesn't work because the map call is expecting a function with argument of type Box[_], and we are trying to give it a function with a type argument EE and an argument of type Box[EE]. The signatures are not compatible. Maybe they could be, and maybe they should be, but with the current compiler they are not.



No comments:

Post a Comment