Monday, December 22, 2008

Scala Constructors



Tonight at BASE, I had a rant about Scala constructors. So I'll just continue the rant here. Constructors seem great in Scala. At first. They give you some great syntactic sugar where it creates accessors/mutators all in one shot:

class Stock(val name:String, val symbol:String, var price:Double, var change:Double){
}

This lets you do nice things like :

val stock = new Stock("Apple Computers", "AAPL", 94.77, -1.11)
println(stock.symbol) // works great
stock.price = 95 // works good, price is var
stock.symbol = "APPL" // won't compile, symbol is a val

Yay, no getter/setter garbage. But what about overloaded constructors? You can kind of do that...

class Stock(val name:String, val symbol:String, var price:Double, var change:Double){
def this(name:String, symbol:String) = this(name,symbol, 0.0, 0.0)
}

So in Scala you can do implement the telescoping constructor anti-pattern. Nice. But what if you got your stock data as a CSV from Yahoo's web service? You need to do some parsing. You might think this will work:

class Stock(name:String, symbol:String, var price:Double, var change:Double){
def this(name:String, symbol:String) = this(name,symbol, 0.0, 0.0)
def this(csv:String) = {
val params = csv.split(",")
name = params(0)
symbol = params(1)
price = java.lang.Double.parseDouble(params(2))
change = java.lang.Double.parseDouble(params(3))
}
}

Nope, this won't work. You can only do a single statement in the 'this' constructor, and it must be to either the main constructor or another 'this' constructor. No extra code. Bill Veneers pointed out that this often leads to code like the following:

case class Stock(val name:String, val symbol:String, var price:Double, var change:Double){
def this(name:String, symbol:String) = this(name,symbol, 0.0, 0.0)
def this(ser:String) = this(parseName(ser), parseSymbol(ser), parsePrice(ser), parseChange(ser))

def parseName(ser:String) = ser.split(",")(0)
def parseSymbol(ser:String) = ser.split(",")(1)
def parsePrice = java.lang.Double.parseDouble(ser.split(",")(2))
def parseChange = java.lang.Double.parseDouble(ser.split(",")(3))
}

Oy. I think even the most enthusiastic Scala programmer would agree that is some very smelly code (and inefficient to boot.) A more common pattern is to use a factory object:

object Stock{
def apply(ser:String):Stock = {
val params = ser.split(",")
new Stock(params(0), params(1), java.lang.Double.parseDouble(params(2)), java.lang.Double.parseDouble(params(3)))
}
}
class Stock(val name:String, val symbol:String, var price:Double, var change:Double){
def this(name:String, symbol:String) = this(name,symbol, 0.0, 0.0)
}

Having a singleton object and a class by the same name is a construct introduced in Scala 2.7. Now usage looks like this:

val apple = new Stock("Apple Computers", "AAPL", 94.77, -1.11)
val microsoft = Stock("Microsoft,MSFT,21.20,-0.10")

Kind of inconsistent, no? In one place you use the new, but to get the benefit of the factory, you can't use new. So usually people change the class to a case class:

object Stock{
def apply(ser:String):Stock = {
val params = ser.split(",")
new Stock(params(0), params(1), java.lang.Double.parseDouble(params(2)), java.lang.Double.parseDouble(params(3)))
}
}
case class Stock(name:String, symbol:String, var price:Double, var change:Double){
def this(name:String, symbol:String) = this(name,symbol, 0.0, 0.0)
}

Now usage is more uniform:

val apple = Stock("Apple Computers", "AAPL", 94.77, -1.11)
val microsoft = Stock("Microsoft,MSFT,21.20,-0.10")
val test = Stock("Test Stock", "TEST")

I guess that is ok. Becuase Stock is now a case class, you don't have to declare name and symbol as public vals. I kind of like using 'new' and I really don't like having to create both an object and a class just to get overloaded constructors. I think it is still a code smell.

Update: In the comments it was pointed out that the companion object pattern (object and class of the same name) has been around for a relatively long time. What was introduced in Scala 2.7 was allowing case classes to have companion objects. So the last version of the code will not compile on anything but Scala 2.7+, but if you make the Stock class a normal class then it will. Of course then you are back to the problem of having two different syntaxes for the constructor, one that needs the 'new' keyword and one that does not (and cannot.)

No comments: