views:

252

answers:

1

I would like to add a method to scala.Enumeration. My first approach was to try to extend it, but was bitten by this. My second approach was to try to define a method, and pass in the Enumeration - if that worked, I hoped to use an implicit conversion. I'm having a hard time preserving type with the return type of the method, however.

object EnumExample {
  object SampleEnum extends Enumeration {
    val include, exclude = Value
  }

  def parse[T <: Enumeration](name:String, enum:T):T#Value =
    enum.valueOf(name) match {
      case Some(x) => x
      case x => throw new RuntimeException("No field named '" + name + "' found on enum " + enum + ", legal values = " + enum.values)
    }

  def main(args:Array[String]) = {
    //compiles fine, and preserves custom type
    val withNameExample:SampleEnum.Value = SampleEnum.withName("include")

    //also fine, but we lost type info
    val enumWithHash:Enumeration#Value = parse("include", SampleEnum)

  /**
  error: type mismatch;
   found   : Main.$anon.EnumExample.SampleEnum#Value
   required: Main.$anon.EnumExample.SampleEnum.Value
      val parseExample:SampleEnum.Value = parse("include", SampleEnum)
   *
   */
    val customTypeWithHash:SampleEnum.type#Value = parse("include", SampleEnum)

    //same error
    val customTypeWithDot:SampleEnum.Value = parse("include", SampleEnum)
  }
}

One obvious fix would be to just remove the return type declaration from the parse method, but that gives me a "illegal dependent method type". This leaves me with lots of questions:

  1. Is this possible to specify? One way or another, I'd like to get a nice error message when parsing an enumeration field in from a String.

  2. Why do I get the "illegal dependent method type"?

  3. What precisely is the "#" operator(?) in this case?

+4  A: 

This looks like a bug to me (at least in 2.8.0 Beta1 where I've tested it).

Particularly instructive is the following:

scala> var x: SampleEnum.type#Value = null
x: SampleEnum.Value = null

Here, we're requesting the arbitrary inner type but we're actually getting the specific inner type. That's just broken (and I will file a bug report if there is not already one, unless someone else swiftly explains why this is not a bug).

So, what to do? Well, first, let's understand the original method signature of parse:

def parse[T <: Enumeration](name:String, enum:T):T#Value

We have T which is a subclass of Enumeration, enum which is an instance of T, and--since there is no way to express that the Value must be from that particular instance of T, we have to resort to T#Value (i.e. an inner type of T, without regard to which particular T it comes from).

Now we have to pass in a specific object, get back a generic inner object, and do it in the face of ExampleObject.type#Value being the same as ExampleObject.Value even though they type differently.

So we have to write our own object from scratch:

class SampleWorkaroundClass extends Enumeration {
  val include, exclude = Value
}
lazy val SampleWorkaround = new SampleWorkaroundClass

Here we have a single instantiation of a specially defined class. Now we can get around the bug in Object:

scala> val typeWorks:SampleWorkaroundClass#Value = parse("include",SampleWorkaround)
typeWorks: SampleWorkaroundClass#Value = include

(The lazy val is just to get exactly the same behavior as with objects where they're not instantiated until they're used; but a val alone would be fine.)


Edit: Outer#Inner means "any inner class of type Inner coming from this outer class" as opposed to myOuter.Inner which means "only that class of type Inner that has this instance of Outer, myOuter, as its enclosing class". Also, I don't get the dependent type error in 2.8.0 Beta1--but not being able to specify the type makes things quite awkward.


Edit: update on bug report--the way it works now is apparently intentional. To use types in this fashion, you are intended to explicitly specify the type in the function call (as it is not inferred to be what you want), like so

val suggested: SampleEnum.type#Value = parse[SampleEnum.type]("include",SampleEnum)

This way is easier if you only have to do this a few times. If you have to do it many times, creating your own class with a val (or lazy val) instantiation probably makes things easier / more compact.

Rex Kerr
Much better! I built on your recommendation and got rid of the second argument by using implicit args: def parse[T <: Enumeration](name:String)(implicit m: Manifest[T]):T#Value = { val enum = m.erasure.getFields().filter(_.getName() == "MODULE$")(0).get(null).asInstanceOf[T] //wow, yuck enum.valueOf(name) match { case Some(x) => x case x => throw new RuntimeException("No field named '" + name + "' found on enum " + enum + ", legal values = " + enum.values) } }Is there a better way to get an object instance from its class object?
Adam Rabung
I am afraid I'm not familiar enough with manifests to know whether your method is generally workable. Keep in mind that all that manifest-fiddling is going to make the code slower. Usually this is irrelevant, but if you plan on using this to do the main job of parsing huge text files, you might need to use a different design.
Rex Kerr