scalaobjectinitializationlazy-initializationscala-3

Scala initialization order object vs. val


In the following program an object is reported as null, even though it is clearly initialized.

class MyClass()
class MyOtherClass(c: MyClass)
object Foo {
  val myClass = new MyClass()
  object otherClass extends MyOtherClass(myClass)
  val otherClasses = Seq(otherClass)
}

object Main {
  def main(args: Array[String]): Unit = {
      println(Foo.otherClass)
      println(Foo.otherClasses)
  }
}

Prints:

Foo$otherClass$@5410511c
List(null)

For now I resolved it by making otherClasses lazy. It is also resolved by changing the argument to MyOtherClass to null, but that's not an option for me.

I always assumed the initialization order of objects and classes in scala to be top to bottom. Clearly I assumed wrong. It looks like vals are initialized first. Is this an edge case or a bug? I think the behaviour in scala 2 and 3 (both 3.3.3. and 3.4.2, in scastie) is the same, even.


Solution

  • objects are initialized lazily for years. In Scala 2.13 spec we read:

    The object definition defines a single object (or: module) conforming to the template tt. It is roughly equivalent to the following definition of a lazy value:

    lazy val mm = new scsc with mt1mt1​ with …… with mtnmtn​ { this: m.typem.type => statsstats }
    

    Note that the value defined by an object definition is instantiated lazily. The new mm$cls constructor is evaluated not at the point of the object definition, but is instead evaluated the first time mm is dereferenced during execution of the program (which might be never at all). An attempt to dereference mm again during evaluation of the constructor will lead to an infinite loop or run-time error. Other threads trying to dereference mm while the constructor is being evaluated block until evaluation is complete.

    Scala 3 didn't change that behavior (although Scala 3 hasn't published its spec yet, so I cannot set it's according to spec, though when it will be published I'm sure it will be the expected behavior).

    If you know that object otherClass is lazy value then null makes perfect sense. The order of the initialization IS from the top to the bottom BUT only eager values are initialized, and when it comes to lazy vals (and objects) you have to be extra careful and NOT evaluate them in the constructor.

    Why objects are lazy? Well, if you think about it, one object constructor using another object and vice versa (normal situation), or object and its companion class referring each other could create cyclical dependency during initialization, and making them lazy is the only sane way of preventing things exploding in the runtime.