2

UPDATE - 2014/Sep/17

It turns out that even the solution in the prior update (from 2013/Feb/19) fails to work if one places println(Value.Player2) as the first command; i.e. the ordinals are still assigned incorrectly.

I have since created a verifiable working solution as a Gist. The implementation waits to assign the ordinals until after all JVM class/object initialization completes. It also facilitates extending/decorating each enumeration member with additional data while still being very efficient for (de)serialization.

I have also created a StackOverflow answer which elaborates on all the different enumeration patterns being used in Scala (including the solution in the Gist I mention above).


I am working with a fresh install of the TypeSafe IDE (Eclipse with ScalaIDE pre-installed). I'm on Windows 7-64bit. And I have had mixed success with the Scala Worksheet. It has already hard crashed my machine (to a full reset or once to the blue screen of death) three times in less than an hour. So, this may be a bug in the Scala Worksheet. I'm not sure yet and don't have time to chase down that issue. However, this enum issue is stopping me from testing.

I am using the following code in the Scala Worksheet:

package test

import com.stack_overflow.Enum

object WsTempA {
  object Value extends Enum {
    sealed abstract class Val extends EnumVal
    case object Empty   extends Val; Empty()
    case object Player1 extends Val; Player1()
    case object Player2 extends Val; Player2()
  }

  println(Value.values)
  println(Value.Empty)
}

The above works fine. However, if you comment out the first println, the second line throws an exception: java.lang.ExceptionInInitializerError. And I am just enough of a Scala newbie to not understand why it's occurring. Any help would be deeply appreciated.

Here's the stack trace from the right side of the Scala Worksheet (left side stripped to display nicely here):

java.lang.ExceptionInInitializerError
    at test.WsTempA$Value$Val.<init>(test.WsTempA.scala:7)
    at test.WsTempA$Value$Empty$.<init>(test.WsTempA.scala:8)
    at test.WsTempA$Value$Empty$.<clinit>(test.WsTempA.scala)
    at test.WsTempA$$anonfun$main$1.apply$mcV$sp(test.WsTempA.scala:14)
    at org.scalaide.worksheet.runtime.library.WorksheetSupport$$anonfun$$exe
 cute$1.apply$mcV$sp(WorksheetSupport.scala:76)
    at org.scalaide.worksheet.runtime.library.WorksheetSupport$.redirected(W
 orksheetSupport.scala:65)
    at org.scalaide.worksheet.runtime.library.WorksheetSupport$.$execute(Wor
 ksheetSupport.scala:75)
    at test.WsTempA$.main(test.WsTempA.scala:11)
    at test.WsTempA.main(test.WsTempA.scala)
 Caused by: java.lang.NullPointerException
    at test.WsTempA$Value$.<init>(test.WsTempA.scala:8)
    at test.WsTempA$Value$.<clinit>(test.WsTempA.scala)
    ... 9 more

The class com.stack_overflow.Enum comes from this StackOverflow thread. I have pasted in my version here for simplicity (in case I missed something critical during the copy/paste operation):

package com.stack_overflow

//Copied from https://stackoverflow.com/a/8620085/501113
abstract class Enum {

  type Val <: EnumVal

  protected var nextId: Int = 0

  private var values_       =       List[Val]()
  private var valuesById_   = Map[Int   ,Val]()
  private var valuesByName_ = Map[String,Val]()

  def values       = values_
  def valuesById   = valuesById_
  def valuesByName = valuesByName_

  def apply( id  : Int    ) = valuesById  .get(id  )  // Some|None
  def apply( name: String ) = valuesByName.get(name)  // Some|None

  // Base class for enum values; it registers the value with the Enum.
  protected abstract class EnumVal extends Ordered[Val] {
    val theVal = this.asInstanceOf[Val]  // only extend EnumVal to Val
    val id = nextId
    def bumpId { nextId += 1 }
    def compare( that:Val ) = this.id - that.id
    def apply() {
      if ( valuesById_.get(id) != None )
        throw new Exception( "cannot init " + this + " enum value twice" )
      bumpId
      values_ ++= List(theVal)
      valuesById_   += ( id       -> theVal )
      valuesByName_ += ( toString -> theVal )
    }
  }
}

Any sort of guidance would be greatly appreciated.


UPDATE - 2013/Feb/19

After several cycles with Rex Kerr, here is the updated versions of the code that now works:

package test

import com.stack_overflow.Enum

object WsTempA {
  object Value extends Enum {
    sealed abstract class Val extends EnumVal
    case object Empty   extends Val {Empty.init}   // <---changed from ...Val; Empty()
    case object Player1 extends Val {Player1.init} // <---changed from ...Val; Player1()
    case object Player2 extends Val {Player2.init} // <---changed from ...Val; Player2()
    private val init: List[Value.Val] = List(Empty, Player1, Player2) // <---added
  }

  println(Value.values)
  println(Value.Empty)
  println(Value.values)
  println(Value.Player1)
  println(Value.values)
  println(Value.Player2)
  println(Value.values)

package com.stack_overflow

//Copied from https://stackoverflow.com/a/8620085/501113
abstract class Enum {

  type Val <: EnumVal

  protected var nextId: Int = 0

  private var values_       =       List[Val]()
  private var valuesById_   = Map[Int   ,Val]()
  private var valuesByName_ = Map[String,Val]()

  def values       = values_
  def valuesById   = valuesById_
  def valuesByName = valuesByName_

  def apply( id  : Int    ) = valuesById  .get(id  )  // Some|None
  def apply( name: String ) = valuesByName.get(name)  // Some|None

  // Base class for enum values; it registers the value with the Enum.
  protected abstract class EnumVal extends Ordered[Val] {
    val theVal = this.asInstanceOf[Val]  // only extend EnumVal to Val
    val id = nextId
    def bumpId { nextId += 1 }
    def compare(that: Val ) = this.id - that.id
    def init() {   // <--------------------------changed name from apply
      if ( valuesById_.get(id) != None )
        throw new Exception( "cannot init " + this + " enum value twice" )
      bumpId
      values_ ++= List(theVal)
      valuesById_   += ( id       -> theVal )
      valuesByName_ += ( toString -> theVal )
    }
  }
}
Community
  • 1
  • 1
chaotic3quilibrium
  • 5,066
  • 5
  • 48
  • 73
  • Could you post the full stacktrace? :) (If you can't get it from the UI have a look in workspace/.metadata/.log) – Brian Smith Feb 18 '13 at 23:56
  • @BrianSmith Tyvm for the request. I added the stack trace above. However, it cuts off in the Scala Worksheet (with 9 lines missing). If you need me to to dig down into the log you mentioned, I will do so. – chaotic3quilibrium Feb 19 '13 at 00:04
  • I have now dug into the log - and this error is not there. Another exception is there which appears to be completely unrelated. Let me know if you still want me to dig around. – chaotic3quilibrium Feb 19 '13 at 00:11
  • No need to call `Empty.init`--just `init` is fully adequate. – Rex Kerr Feb 19 '13 at 15:13
  • Also, I note that this is a pretty inadequate solution overall for enums, as you have to list every item twice and you have ~35 characters of boilerplate to type for each one also. At least it gives you warnings if you miss one in a match statement! – Rex Kerr Feb 19 '13 at 15:25
  • And it has another issue - if one places places a println(Value.Player2) as the first command above, the id ordering gets messed up. I am now working on starting from scratch and seeing what I can work up that is less mutable and less reliant on client boiler plate (although I am fine with a certain amount of internal implementation boilerplate). – chaotic3quilibrium Feb 20 '13 at 14:53
  • Per the "UPDATE - 2014/Sep/17" above, I have finally solved the "id ordering problem" which occurs "if one places places a println(Value.Player2) as the first command above". It is located at the following Gist: https://gist.github.com/chaotic3quilibrium/57add1cd762eb90f6b24 – chaotic3quilibrium Sep 18 '14 at 02:38

1 Answers1

2

There are two problems here: one is that the code fails to work due to initialization issues, and the other is that you are having IDE problems. I'm going to address only the code failure.

The problem is that you need Empty() run before you actually access Empty, but accessing inner case objects doesn't run the initializer on the outer object since they're only pretending to be members of the inner object. (There is no variable inside Value that holds Empty.)

You can bypass this problem by running the apply() method as part of the initializer for Empty:

object Value extends Enum {
  sealed abstract class Val extends EnumVal
  case object Empty   extends Val { apply() }
  case object Player1 extends Val { apply() }
  case object Player2 extends Val { apply() }
}

Now your initialization error should go away. (Because you have to do it this way, I suggest that apply() is actually a bad choice of name; maybe set or somesuch would be better (shorter).

If you stick the println into a main method, here's what the bytecode looks like for printing Value.values:

public void main(java.lang.String[]);
  Code:
   0:   getstatic   #19; //Field scala/Predef$.MODULE$:Lscala/Predef$;
   3:   getstatic   #24; //Field WsTempA$Value$.MODULE$:LWsTempA$Value$;
   6:   invokevirtual   #30; //Method Enum.values:()Lscala/collection/immutable/List;
   9:   invokevirtual   #34; //Method scala/Predef$.println:(Ljava/lang/Object;)V
   12:  return

Note line 3, where you get a static field (which means that the JVM ensures that the field is initialized) for Values itself. But if you go for Empty you get

public void main(java.lang.String[]);
  Code:
   0:   getstatic   #19; //Field scala/Predef$.MODULE$:Lscala/Predef$;
   3:   getstatic   #24; //Field WsTempA$Value$Empty$.MODULE$:LWsTempA$Value$Empty$;
   6:   invokevirtual   #28; //Method scala/Predef$.println:(Ljava/lang/Object;)V
   9:   return

Now line 3 refers not to Values but to the inner object, which means the inner object's initializer gets called first, which then calls the outer object's initializer, which then sees that the inner object's initializer is supposed to be done (but it's actually not done)...and it calls a method on it and...boom.

If you put the apply inside the Empty initializer, you are saved because the Value initializer, even though it's called out of order, doesn't call methods on Empty any longer. So it miraculously works out. (Just make sure you don't introduce any other method calls!)

Rex Kerr
  • 162,454
  • 26
  • 308
  • 402
  • Awesome sauce! That did the trick. My typical choice in this situation is to change from apply to init. Does that conflict with any sort of other pattern (similar to how apply conflicts)? – chaotic3quilibrium Feb 19 '13 at 01:27
  • No, `init` should be fine. – Rex Kerr Feb 19 '13 at 01:28
  • Oops. While the initialization error went away, another problem popped up. The first println is now consistently returning an empty list. – chaotic3quilibrium Feb 19 '13 at 01:36
  • That is still not working. Even with the change, the Value.values call is returning an empty list. As long as I hit all three of the instances, then Value.values appears to correctly return all three instances. So, I really need both - to be able to call an instance and have it initialize all three values. Or to call the Value.values (and related calls at the Enum level) and have all three values initialize. – chaotic3quilibrium Feb 19 '13 at 01:53
  • I added one final line to the Value class: private val init: List[Value.Val] = List(Empty, Player1, Player2) - this ensures that if Value methods are called first, then it invokes all of the enumerations. And if a value is directly accessed, then the other values end up being instantiated at the same time. Hope that makes sense. – chaotic3quilibrium Feb 19 '13 at 01:55
  • I have added an UPDATE to my original post which captures the changes to both files. Please let me know if you see anything incorrect or inaccurate with it. And tysvm for your help. – chaotic3quilibrium Feb 19 '13 at 02:06
  • I have added an additional update to my original post. Even with the changes from your answer applied, the JVM class/object initialization order problem still appeared; i.e. if one places println(Value.Player2) as the first command, the ordinals are still assigned incorrectly). The update goes into lots of detail. I did solve the JVM class/object initialization problem. It is in the following Gist: https://gist.github.com/chaotic3quilibrium/57add1cd762eb90f6b24 – chaotic3quilibrium Sep 19 '14 at 20:16