Companion Objects as Classes in Scala

Tuesday, June 15, 2010
File under: Machine Room

Here is a little pattern I came across. It basically shows how companion objects work like class objects in Ruby and how type inference makes working with such types quite painless. I’m not sure if this is already widely known but a quick search on Google doesn’t reveal anything similar.

Types are not really first-class objects in Scala (and in Java, too). In generics, they are removed at compile time through type erasure, and you cannot simply pass a class to a method by saying method(ClassName).

Most of the time, this is not an issue, but sometimes it would be very handy to pass a class to a method, for example when you need to create new objects of a given type. One example I came across was when working on the new Cassandra-based backend for twimpact. By default, Cassandra only supports storing byte arrays, and we needed some infrastructure to serialize objects into byte arrays and back (without using standard Java serialization). Now serializing an object is simple enough: you just write a trait which provides a function for serializing. However, deserializing is a bit harder because you don’t have an object available. So the question is, how does the program know how to deserialize?

In Ruby, you would probably just pass the class object and work with the class methods like this:

class StoredNumber
  def self.from_byte_array(bytes)
    i = bytes[3] << 24 | bytes[2] << 16 | bytes[1] << 8 | bytes[0]
    StoredNumber.new(i)
  end

  def initialize(i)
    @value = i
  end

  def to_byte_array
    [ (@value >> 24) & 0xff,
      (@value >> 16) & 0xff,
      (@value >> 8) & 0xff,
      @value & 0xff ]
  end
end

# and then, you can store a hyptothetical element to a store and
# convert it back by passing in the class:

Store.put(StoredNumber.new(3))

x = Store.get(StoredNumber)

The storing part could be done in the same way in Scala (of course with explicitly introducing a trait for the conversion part.)

trait ConvertsToBytes {
  def toByteArray(): Array[Byte]
}

class StoredNumber(value: Int) extends ConvertsToBytes {
  def toByteArray(): Array[Byte] =
     // same blah as above
}

// Store it like this
Store.put(new StoredNumber(42))

To retrieve an object, you could pass a converter function like this:

def numberFromBytes(bytes: Array[Bytes]): StoredNumber {
  // convert and extract from bytes
}

// this is how Store would have to be defined
class Store {
   // ...
   def get[T](convert: (Array[Bytes]) => T): T = //...
}

// Get a number (without type inference)
Store.get[StoredNumber](numberFromBytes)

You can actually drop the type parameter on the call to get

Store.get(numberFromBytes) // Type StoredNumber is inferred by Scala.

Still, this is not as elegant as the Ruby version because the class and the converter are separate entities.

The solution is to use another trait for the conversion back and let the companion object implement that trait:

// Note that we have to put in the result as a type parameter.
trait ConvertsFromBytes[T] {
  def fromBytes(bytes: Array[Byte]): T
}

// Note that we need to explicitly name StoredNumber when
// extending ConvertsFromBytes
object StoredNumber extends ConvertsFromBytes[StoredNumber] {
  def fromBytes(bytes: Array[Byte]): StoredNumber =
    // ... convert back from array
}

// This is how Store would have to be implemented now... .
class Store {
   def get[T](type: ConvertsFromBytes[T]): T = 
     // ... convert back using type.fromBytes()
}

// Without using type inference, we would have to say
Store.get[StoredNumber](StoredNumber)

// but with type inference, we can simply say the following
// which is just as compact as passing the class object.
Store.get(StoredNumber)

On closer inspection, the construction is very similar to the class objects in Ruby. The only difference is that you have to explicitly define a trait for the methods you expect, which isn’t so surprising after all. The rest is taken care of by type inference.

Posted by Mikio L. Braun at 2010-06-15 00:00:00 +0000

blog comments powered by Disqus