Friday, October 12, 2012

Duck typing is like the force


Scala's type system is impressive, and yesterday one feature came in handy: converting certain classes into a common type with useful methods on it.

Say several types have a core set of characteristics, and we want to define methods based on those without modifying the original classes. In this example, various classes represent regions of a chromosome, and the common characteristics are a begin position, end position, and strand. I want to be able to ask each of these whether they contain a particular position on the chromosome.

A section of something can be efficiently represented by a case class, and methods can be added there.

case class Section (begin: Int, end: Int, moreInfo : Char) {
    def contains(position: Int) : Boolean = { … }
}

Then when I have some other class, say a chunk, that contains the information that defines a Section:
Section(chunk.begin, chunk.end, chunk.moreInfo).contains(positionOfInterest)
But this gets verbose. I want to go straight from the Chunk to the Section without pulling individual fields. I could create a custom apply method in Section's companion object:
object Section {
   def apply(Chunk chunk) : Section = Section(chunk.begin, chunk.end, chunk.moreInfo)
}
Now: 
Section(chunk).contains(positionOfInterest)
But that only works for one input class - I also want this to work for wads, hunks, etc. For a more general approach, a structural type works better:
object Section {
    type T = { 
    def begin: Int;
    def end: Ind; 
    def moreInfo: Char }

   def apply(thing : T) : Section = 
     Section(thing.begin, thing.end, thing.moreInfo)
}
This apply method will convert anything with the right properties into a Section.
Section(chunk).contains(positionOfInterest)
Section(wad).contains(positionOfInterest)

Section(hunk).contains(positionOfInterest)
It would be even more fun to make the apply method implicit and start using the contains method directly on the arbitrary areas. This doesn't work, though. Scala won't do implicit conversions into structural types. That would get ridiculous.

Duck typing is useful this way. I feel all clever for figuring out how to do this -- a common feeling among those who play with the Scala type system. However, if I have control over the source code of Chunk, Wad, and the other types I want to call contains on, the correct way to do this is with a trait:

trait Section {
   def begin: Int
   def end: Int
   def moreInfo: Char

   def contains(position: Int) : Boolean = {…}
}

Add this trait to the declaration of the classes that represent areas, and the contains method works even better. No additional code is required inside the implementing classes; they already define the abstract fields listed in the trait. Things just work.
class Chunk extends …. with Section {…}
chunk.contains(positionOfInterest)
or,
class Wad extends Section {…}
wad contains positionOfInterest
Traits are the better way to add common methods defined in terms of common fields. They give Scala more information at compile time. Now contains show up in autocompletion in the REPL or the IDE.

Therefore, use the force -- I mean, duck typing -- as a last resort. It's powerful when we can't modify class definitions, but honest, up-front static typing is better.

2 comments:

  1. Style comment: the text color for "common type" and "useful methods" make the text look a lot like a link. Consider a different color/font.

    ReplyDelete
  2. "Duck typing is useful this way. I feel all clever for figuring out how to do this -- a common feeling among those who play with the Scala type system. However, if I have control over the source code of Chunk, Wad, and the other types I want to call contains on.."

    But what does one do if he doesn't have control over the source code of the objects that he is trying to Duck Type? Is this possible?

    ReplyDelete