10

I'm seeing some initialization weirdness when mixing val's and def's in my trait. The situation can be summarized with the following example.

I have a trait which provides an abstract field, let's call it fruit, which should be implemented in child classes. It also uses that field in a val:

scala> class FruitTreeDescriptor(fruit: String) {
     |   def describe = s"This tree has loads of ${fruit}s"
     | }
defined class FruitTreeDescriptor

scala> trait FruitTree {
     |   def fruit: String
     |   val descriptor = new FruitTreeDescriptor(fruit)
     | }
defined trait FruitTree

When overriding fruit with a def, things work as expected:

scala> object AppleTree extends FruitTree {
     |   def fruit = "apple"
     | }
defined object AppleTree

scala> AppleTree.descriptor.describe
res1: String = This tree has loads of apples

However, if I override fruit using a val...

scala> object BananaTree extends FruitTree {
     |   val fruit = "banana"
     | }
defined object BananaTree

scala> BananaTree.descriptor.describe
res2: String = This tree has loads of nulls

What's going on here?

jjst
  • 2,631
  • 2
  • 22
  • 34

3 Answers3

5

In simple terms, at the point you're calling:

val descriptor = new FruitTreeDescriptor(fruit)

the constructor for BananaTree has not been given the chance to run yet. This means the value of fruit is still null, even though it's a val.

This is a subcase of the well-known quirk of the non-declarative initialization of vals, which can be illustrated with a simpler example:

class A {                           
     val x = a
     val a = "String"
}

scala> new A().x
res1: String = null

(Although thankfully, in this particular case, the compiler will detect something being afoot and will present a warning.)

To avoid the problem, declare fruit as a lazy val, which will force evaluation.

mikołak
  • 9,605
  • 1
  • 48
  • 70
2

The problem is the initialization order. val fruit = ... is being initialized after val descriptor = ..., so at the point when descriptor is being initialized, fruit is still null. You can fix this by making fruit a lazy val, because then it will be initialized on first access.

drexin
  • 24,225
  • 4
  • 67
  • 81
1

Your descriptor field initializes earlier than fruit field as trait intializes earlier than class, that extends it. null is a field's value before initialization - that's why you get it. In def case it's just a method call instead of accessing some field, so everything is fine (as method's code may be called several times - no initialization here). See, http://docs.scala-lang.org/tutorials/FAQ/initialization-order.html

Why def is so different? That's because def may be called several times, but val - only once (so its first and only one call is actually initialization of the fileld).

Typical solution to such problem - using lazy val instead, it will intialize when you really need it. One more solution is early intializers.

Another, simpler example of what's going on:

scala> class A {val a = b; val b = 5}
<console>:7: warning: Reference to uninitialized value b
       class A {val a = b; val b = 5}
                        ^
defined class A

scala> (new A).a
res2: Int = 0 //null

Talking more generally, theoretically scala could analize the dependency graph between fields (which field needs other field) and start initialization from final nodes. But in practice every module is compiled separately and compiler might not even know those dependencies (it might be even Java, which calls Scala, which calls Java), so it's just do sequential initialization.

So, because of that, it couldn't even detect simple loops:

scala> class A {val a: Int = b; val b: Int = a}
<console>:7: warning: Reference to uninitialized value b
       class A {val a: Int = b; val b: Int = a}
                             ^
defined class A

scala> (new A).a
res4: Int = 0

scala> class A {lazy val a: Int = b; lazy val b: Int = a}
defined class A

scala> (new A).a
java.lang.StackOverflowError

Actually, such loop (inside one module) can be theoretically detected in separate build, but it won't help much as it's pretty obvious.

Community
  • 1
  • 1
dk14
  • 22,206
  • 4
  • 51
  • 88