Tuesday, November 11, 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.)

14 comments:

Jorge Ortiz said...

I think the more common idiom (at least in the Scala Standard Library), would be to have a "fromCSV" (or "fromCSV") method in the companion object.

val microsoft = Stock.fromCsv("Microsoft,MSFT,21.20,-0.10")

Alex Cruise said...

Scala constructors are a bit annoyingly restrictive, but fairly frequently, when you find some weird corner of Scala, it turns out that it was done that way because it fits within a well-understood theoretical framework. However, I don't know enough about Scala to judge whether the simplified constructor rules fit into this category.

Also, the ability to define a class and an object with the same name isn't new, it's been in the language a very long time. It's pretty much Scala's equivalent of "private static".

Unknown said...

@jorge I like your style as it is more explicit what is going on. However I have seen the other style (leveraging apply) frequently as well.

@arrgh Really? If I take the last version of the code and try to compile it with Scala 2.6.1 (previous version I had installed on my system) then I get:

$ scalac Stock.scala
Stock.scala:7: error: Stock is already defined as object Stock
case class Stock(name:String, symbol:String, var price:Double, var change:Double){
^
one error found

That's why I thought it was new in 2.7. Was there a regression in 2.6?

Alex Cruise said...

@Michael: It used to be that you couldn't define a companion object to a *case class*, which in your example was not the case. :)

Unknown said...

@arrgh Ahh, yes you are right. I tried the code with the Stock class just being a normal class, not a case class and it worked. Thanks for the information. I'll put an addendum to the original post for the sake of accuracy.

Jesper Nordenberg said...

With Scala I tend to use the trait/factory method pattern instead of constructors. IMO it's cleaner, easier to understand and encourage separation of interface and implementation. Your example could be implemented as:

trait Stock {
val name : String
val symbol : String
var price : Double
var change : Double
}

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

Stephan.Schmidt said...

I would use a Builder pattern for complicated instance creation, perhaps your constructors are too complex :-)

Cheers
Stephan
http://twitter.com/codemonkeyism

Unknown said...

@Stephan So do you think the example class above is so complicated that you need an extra (builder) class? I think Scala's constructors are optimized for procedural programming, where you don't really have objects, you have data structures. Even if you do have objects, the syntax invites you to expose all of their internal state. Maybe Scala should advertise itself as a hybrid of object-oriented/functional/procedural!

Tim W said...

I agree -- I'm one day or so into Scala, and while it seem oh-so-clever to have the class constructor declared *right there* on the class itself, it leads to lots of "surprise" by users. (It's also rather weird to see constructor code at the same level as the instance variables and class methods.)

I like many things about Scala so far, but this seems like a really stupid choice, especially given its Java, multi-constructor roots.

Anonymous said...

I believe I've come up with a much more consistent solution than a companion object:

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

def this(t:(String, String, Double, Double)) = this(t._1,t._2,t._3,t._4)

Thom said...

If the new is bothering you (and it does bother me), why not make another apply method on the companion object that takes the same arguments as the constructor? Viola. Identical syntax for both forms.

Cristian Vrabie said...

How about validating the parameters when passed to the main constructor? For example if you need to guarantee that price >= 0. You can do that in the extra constructors by throwing an exception or in the apply method of the companion object. But someone can still call the main constructor with an incorrect value. How would someone go about validating the parameters passed to the main constructor?

Cristian Vrabie said...

My bad. Forgot that in Scala the whole body of the class is your primary constructor, so you can add your validation logic there.

Tim Harper said...

Yeah... just go with the factory pattern. It's actually really nice in practice.