Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

case object gets initialized to null - how is that even possible?

Tags:

scala

I'm experiencing a bizarre bug. One of the case objects (only one) gets null as its value and obviously causes Null pointer exceptions later in the life. But: why?

I've tried different things (renaming objects, changing hesitance, even rewriting the same line twice, getting one of them to compile nice, while another gives null). I've cloned the repo to make sure it's not some funny transitive effect. Both Scala 2.11.4 and 2.11.5 cause the same.

The code is rather twisty and cutting it off its context makes little sense. But here it is, in case it rings any bells on someone else.

sealed trait A {}

sealed trait B extends A {}

sealed trait C { val n: Int }
object C {
  val C1: C = new C { val n = 1 }
  val C2: C = new C { val n = 2 }
  val C3: C = new C { val n = 3 }
  val C4a: C = new C { val n = 4 }
  val C4b: C = new C { val n = 5 }
}

class D (depends: Seq[C], f: (Seq[Double]) => Double) {
  // ...methods removed
}

sealed trait E { val e: Int = 1 }

object D {
  import C._

  private def f( v: Seq[Double] ): Double = 0

  private def list( acc: Boolean ) = List(
    C1, C2, C3,
    if (acc) C4a else C4b
  )

  case object D1 extends D( list(true), f ) with E   // <-- this gets to be 'null'!!
  case object D2 extends D( list(false), f )

  val keys: Seq[D] = List(D1, D2)

  println(s"D1: $D1")
  println(s"D2: $D2")

  assert(D1 != null)    // <-- caught here
  assert(D2 != null)
}

I tried to keep as much of the details in the code, as possible, in case they matter somehow.

What can this be?

I tagged it as 'heisenbug' but when I compile stuff, it's consistent (i.e. successive test runs would always produce the same effect).

EDIT: I originally said this only happened with sbt test (and not sbt run) but that's not true. Both were equally affected.

EDIT: Seems @dk14 has filed this as Scala issue 9115. Thanks!

like image 969
akauppi Avatar asked Jan 26 '15 13:01

akauppi


People also ask

Can an object be null?

In object-oriented computer programming, a null object is an object with no referenced value or with defined neutral ("null") behavior. The null object design pattern describes the uses of such objects and their behavior (or lack thereof).

How do you make sure an object is not null?

Typically, you'll check for null using the triple equality operator ( === or !== ), also known as the strict equality operator, to be sure that the value in question is definitely not null: object !== null . That code checks that the variable object does not have the value null .


2 Answers

I have no idea what exactly happens, but I suspect there is some kind of construction order involved. I can reproduce the problem 100% of the time, if I paste the code and do

scala> D.D1
D1: null
D2: D2
java.lang.AssertionError: assertion failed
  at scala.Predef$.assert(Predef.scala:151)
  ... 47 elided

Also if I ask for D2 first, it will fail too, so it's not related to E.

scala> D.D2
D1: D1
D2: null
java.lang.AssertionError: assertion failed
  at scala.Predef$.assert(Predef.scala:151)
  ... 47 elided

But if I ask for D first, it works 100% of the time:

scala> D
D1: D1
D2: D2
res1: D.type = D$@50434135

scala> D.D1
res2: D.D1.type = D1

I succeed to distill the problem into shorter code example:

class Bug(val f: Int) {}

object Bug {
  private def f(n: Int): Int = n

  case object Bug1 extends Bug(f(1))
  case object Bug2 extends Bug(f(2))

  val values: List[Bug] = List(Bug1, Bug2)

  println(s"1: $Bug1")
  println(s"2: $Bug2")
}

Asking for Bug.Bug1 will fail. Yet if I remove println lines - everything works. Or "execute" Bug first. Yet having values is ok.

I guess some initialisation locks go wrong way, when asking directly for Bug.Bug1 or Bug.Bug2.

like image 112
phadej Avatar answered Nov 08 '22 06:11

phadej


It's not working only when you directly access D.D1 or D.D2 before D object itself as it's not initialized here, so case object values are still null's. Seems like a bug for me (should initialize D before calling D.D1). Workaround: just call D itself (to run initialization) somewhere before D.D1. See, https://stackoverflow.com/a/26072435/1809978 - lazy initialization is actually a feature, but not in the case you've found. Simplified example:

object D {
  def aaa = 1 //that’s the reason
  class Z (depends: Any)
  case object D1 extends Z(aaa) // <-- this gets to be 'null'!!
  case object D2 extends Z(aaa)
  println(D1) 
  println(D2) 
}

Results :

defined object D
scala> D.D1
null
D2
res32: D.D1.type = D1

After re-definition of D:

defined object D
scala> D.D2
D1
null
res34: D.D2.type = D2

So scala forgots to initialize the requested sub-object (if it refers to some another member of object inside sub-object's definition) before running enclosing object initialization. It initializes this sub-object after (when aaa is defined). I think primary intention was to initialize all referred members before requested member, but it's still seems a bug as it changes intialization order, depending on how D was used in the code.

like image 22
dk14 Avatar answered Nov 08 '22 06:11

dk14