Monday, March 1, 2010

NullPointer when mixed traits (Warning)

This tip is mainly to document a 'GOTCHA' that I got caught by recently. It basically goes like this:

Trait Y extends(or has self-type) X. Trait X defines some abstract method 'm'. The initialization code in Y accesses 'm'. Creation of an object new X with Y results in: *Boom* NullPointerException (on object creation).

The example in code:
  1. scala> trait X { val x : java.io.File }
  2. defined trait X
  3. scala> trait Y {self : X => ; val y = x.getName} 
  4. defined trait Y
  5. scala> new X with Y { val x = new java.io.File("hi")}
  6. java.lang.NullPointerException
  7. at Y$class.$init$(< console>:5)
  8. at $anon$1.< init>(< console>:7)
  9. ...

At a glance it seems that x should override the abstract value x in trait X. However the order in which traits are declared is important. In this case first Y is configured then X. Since X is not yet configured Y throws an exception. There are several ways to work around this.
Option 1:
  1. trait X {val x : java.io.File}
  2. trait Y {self : X => ; val y = x.getName}
  3. /*
  4. Declaring Y with X will work because Y is initialized after X
  5. but remember that there may
  6. be other reasons that X with Y is required.  
  7. Method resolution is one such reason
  8. */
  9. new Y with X { val x = new java.io.File("hi")}

Option 2:
  1. trait X { val x : java.io.File }
  2. trait Y {self : X => ; def y = x.getName}
  3. /*
  4. Since method y is a 'def' x.getName will not be executed during initialization.
  5. */
  6. scala> new X with Y { val x = new java.io.File("hi")}
  7. res10: java.lang.Object with X with Y = $anon$1@7cb9e9a3

Option 3:
  1. trait X { val x : java.io.File }
  2. trait Y {self : X => ; lazy val y = x.getName}
  3. /*
  4. 'lazy val' works for the same reason 'def' works: x.getName is not invoked during initialization
  5. */
  6. scala> new X with Y { val x = new java.io.File("hi")}
  7. res10: java.lang.Object with X with Y = $anon$1@7cb9e9a3

Option 4:
  1. trait X {val x : java.io.File }
  2. trait Y extends X {def y = x.getName}
  3. /*
  4. if Y extends X then a new Y can be instantiated
  5. */
  6. new Y {val x = new java.io.File("hi")}

Two more warnings. First, the same error will occur whether 'x' is a def or a val or a var.
  1. trait X { def x : java.io.File }   
  2. trait Y {self : X => ; val y = x.getName}     
  3. new X with Y { val x = new java.io.File("hi")}

Second warning: In complex domain models it is easy to have a case where Y extends X but the final object is created as: new X with Y{...}.

You will get the same error here because (I think) the compiler recognized that Y is being mixed in with X and therefore the X will be initialized as after Y instead of before Y.

First the code:
  1. trait X { def x : java.io.File }   
  2. trait Y extends X { val y = x.getName}        
  3. new X with Y { val x = new java.io.File("hi")}

If the code instantiated new Y{...} the initialization would be X then Y. Because X can only be initialized once, the explicit declaration of new X with Y forces Y to be initialized before X. (X can only be initialized once even when it appears twice in the hierarchy).

This is a topic called linearization and will be addressed in the future.

4 comments:

  1. Option 2 is no different from original, should swap "val" for "def"

    ReplyDelete
  2. Option 3 lacks keyword "lazy"?

    ReplyDelete
  3. Option 1 also throws NPE (scala 2.10.3)

    ReplyDelete