GKelly GKelly - 3 months ago 22
Scala Question

Scala singleton factories and class constants

OK, in the question about 'Class Variables as constants', I get the fact that the constants are not available until after the 'official' constructor has been run (i.e. until you have an instance). BUT, what if I need the companion singleton to make calls on the class:

object thing {
val someConst = 42
def apply(x: Int) = new thing(x)
}

class thing(x: Int) {
import thing.someConst
val field = x * someConst
override def toString = "val: " + field
}


If I create companion object first, the 'new thing(x)' (in the companion) causes an error. However, if I define the class first, the 'x * someConst' (in the class definition) causes an error.

I also tried placing the class definition inside the singleton.

object thing {
var someConst = 42

def apply(x: Int) = new thing(x)

class thing(x: Int) {
val field = x * someConst
override def toString = "val: " + field
}
}


However, doing this gives me a 'thing.thing' type object

val t = thing(2)


results in

t: thing.thing = val: 84


The only useful solution I've come up with is to create an abstract class, a companion and an inner class (which extends the abstract class):

abstract class thing

object thing {
val someConst = 42
def apply(x: Int) = new privThing(x)

class privThing(x: Int) extends thing {
val field = x * someConst
override def toString = "val: " + field
}
}

val t1 = thing(2)
val tArr: Array[thing] = Array(t1)


OK, 't1' still has type of 'thing.privThing', but it can now be treated as a 'thing'.

However, it's still not an elegant solution, can anyone tell me a better way to do this?

PS. I should mention, I'm using Scala 2.8.1 on Windows 7

Answer

First, the error you're seeing (you didn't tell me what it is) isn't a runtime error. The thing constructor isn't called when the thing singleton is initialized -- it's called later when you call thing.apply, so there's no circular reference at runtime.

Second, you do have a circular reference at compile time, but that doesn't cause a problem when you're compiling a scala file that you've saved on disk -- the compiler can even resolve circular references between different files. (I tested. I put your original code in a file and compiled it, and it worked fine.)

Your real problem comes from trying to run this code in the Scala REPL. Here's what the REPL does and why this is a problem in the REPL. You're entering object thing and as soon as you finish, the REPL tries to compile it, because it's reached the end of a coherent chunk of code. (Semicolon inference was able to infer a semicolon at the end of the object, and that meant the compiler could get to work on that chunk of code.) But since you haven't defined class thing it can't compile it. You have the same problem when you reverse the definitions of class thing and object thing.

The solution is to nest both class thing and object thing inside some outer object. This will defer compilation until that outer object is complete, at which point the compiler will see the definitions of class thing and object thing at the same time. You can run import thingwrapper._ right after that to make class thing and object thing available in global scope for the REPL. When you're ready to integrate your code into a file somewhere, just ditch the outer class thingwrapper.

object thingwrapper{
   //you only need a wrapper object in the REPL
   object thing {
       val someConst = 42
       def apply(x: Int) = new thing(x)
   }   

   class thing(x: Int) {
       import thing.someConst
       val field = x * someConst
       override def toString = "val: " + field
   }   
}