4
votes

Context

We are working on a static verifier for Scala programs (early work described in this Master's thesis), and the current focus lies on verifying Scala features that involve lazy evaluation. We are mainly interested in the semantics (behaviour) of the features, not in other (nevertheless important) aspects such as comprehensibility or conciseness.

To simplify things, we - for the time being - ignore special roles that singleton objects could have. For example, that some are companion objects (which is probably orthogonal w.r.t. to their lazy nature), or that some are package objects.

Properties of lazy vals and singleton objects

Lazy vals

Assume a lazy val

lazy val v = I

where I is the initialiser block, i.e., the code that determines the value of the lazy val. The initialiser block I is executed when the lazy val v is dereferenced for the first time.

Singleton objects

Assume a singleton object

object Foo {
  C1

  val v1 = I1
  var v2 = I2
  lazy val v3 = I3
  def m() {C2}
}

where C1 is the code making up the constructor of object Foo, where I1 to I3 are again initialiser blocks, and where C2 is the body of method m. When the object Foo is first used (dereferenced, or assigned to a variable/field), then C1, I1 and I2 are executed. I3 is only executed when Foo.v3 is dereferenced (since v3 is a lazy val) and C2 is executed whenever m is called.

Question

Consider this version of Foo, where the singleton object has been encoded by a lazy val and an anonymous class:

// Should probably sit in a package object
lazy val Foo = new {
  C1

  val v1 = I1
  var v2 = I2
  lazy val v3 = I3
  def m() {C2}
}

Can anybody think of a reason for why the encoding of the singleton object Foo as a lazy val would show a different behaviour than the original singleton object? That is, are there (corner) cases where the encoded version would have a different semantics than the original code?

1
This is a good question—see for example this answer and the comment by Miles Sabin for one example of a case where there is a difference (not sure about the should be, though).Travis Brown
Thanks for the pointer! My interpretation of Miles' comment, though, is that the implementation of Poly1 won't compile if the object were encoded as a lazy val, and that the reason is essentially due to name resolution. This was not quite the difference I had in mind, because code that is rejected by the compiler sort of obviously shows a different behaviour. Moreover, if the problem really is "just" due to name resolution, then it should be possible to resolve it in a way that does not affect the language semantics. But of course that's just a guess on my side ...Malte Schwerhoff

1 Answers

0
votes

There is at least one major semantic difference, which I explained herebelow. I think there are some corner cases in multi-threaded contexts, because objects are created during the static initializer of their class, and static initializers have a weird interaction with locks. But I am no JVM multi-thread expert, so I don't know the details.

Anyway, here is something that happens even in a single-threaded context (and that is used at least in the implementation of Manifests, and maybe ClassTags, in the current standard library).

Consider the following two objects:

object A {
  val other = B
}
object B {
  val other = A
}

This is valid Scala code, and accessing either A.other or B.other will correctly return the other one. This is because the "val" for A is initialized right after the call to the super constructor. Essentially, you could somehow translate the above object into this "low-level" code:

private var instanceA: AClass = null
def A(): AClass = {
  if (instanceA == null)
    new AClass
  instanceA
}
class AClass {
  def this() = {
    // reification of the constructor of A
    super()
    instanceA = this
    this.other = B()
  }
}

Of course, B is translated similarly. You can see that, at the time B() is called to initialize A.other, the global variable instanceA has already been initialized. Hence, when B is created and accesses A() to initialize B.other, the def A() will return immediately the right instance of A.

lazy vals do not have this early initialization, so encoding objects as simple lazy vals won't fly. Accessing either of the objects will "dead-lock" with the only threaded waiting for itself.