- About Scala
- Documentation
- Code Examples
- Software
- Scala Developers
Alternate approach to Enum
Sun, 2010-09-05, 06:51
I haven't been quite satisfied with enums in Scala. The official approaches seem to be either to extend Enumeration or use sealed case classes. The topic seems to have some coverage on StackOverflow. Extending Enumeration makes it tough to add custom stuff to your objects beyond a simple string and the compiler doesn't know about all of the cases like case classes. However, case classes don't give you the handy ability to iterate through the values.
I also saw that someone else posted a hybrid approach.
After playing with a few ideas, I have a rough solution that scratches my itch a bit better, but introduces it's own dangers. I don't believe that this is "the" good solution. We may need the Scala language to grow a bit more before we get a good robust approach that has all of the good stuff.
I basically just added iteration to the case class approach. This way it is easy to define the base class used by the values, and the compiler can see the objects as a complete set. Because I couldn't do it "right" I added some stuff to help me from doing some obvious dumb stuff.
I used lazy val initialization to help with defining the iteration order of the objects. Objects don't get created until they get referenced, so by defining the order, all of the objects in the order get initialized, and then the defineOrder function "defines" the enum. If another object is later referenced that was not included in the official order, it will complain. Likewise, if you try to do anything interesting without defining order, it will also complain.
There are parts of the approach that are a bit sloppy. I.e. the "values" list is a list of the items as the base "value" class, while the user has to define their own more specifically typed list "order" if needed. I tried to work around this, but the Java erasure and Scala manifests etc. just left me a bit bloodied before I gave up and stuck with the simple approach that worked well enough.
This is my first pass prototype. Seems to do most of what I wanted, but I was interested in feedback.
abstract class AltEnum{ private var orderDefined = false private var privValues:Option[List[Value]] = None lazy val values:List[Value] = { if( ! orderDefined ) throw new Error("Tried to access id without defining order") // this shouldn't be empty if orderDefined if( privValues.isEmpty ) throw new Error("Unexpected empty priv value") privValues.get } def defineOrder[T <: Value](list:List[T]):List[T]={ orderDefined = true var nextId = 0 list.foreach( (v)=>{ v.privId = nextId nextId += 1 v.id // cause id to init }) privValues = Some(list) list }
// look through list and find value with id def getById[T<:Value](id:Int, list:List[T]):Option[T]={ list.find((i)=>{i.id == id}) } // look through list and find value with id def getByName[T<:Value](name:String, list:List[T]):Option[T]={ list.find((i)=>{i.name == name}) }
protected class Value(){ if( orderDefined ) throw new Error("Created value or accessed value that was not in defined order") private[AltEnum] var privId:Int = -1 lazy val id = { if( ! orderDefined ) throw new Error("Tried to access id without defining order") privId } lazy val name = { if( ! orderDefined ) throw new Error("Tried to access name without defining order") this.toString } }}
Here is a sample usage:
object JunkEnum extends AltEnum{ sealed class Junk(val someString:String) extends Value
case object FIRST extends Junk("Wow") case object SECOND extends Junk("No Way") case object THIRD extends Junk("Yah")
val order = defineOrder(FIRST::SECOND::THIRD::Nil)
}
object Main2 { def main(args: Array[String]): Unit = { println("enum list :" + JunkEnum.order) import JunkEnum._ for( j <- order){ println(" "+j.name+" id = "+j.id+" someString = "+j.someString) } }}
Output:
enum list :List(FIRST, SECOND, THIRD) FIRST id = 0 someString = Wow SECOND id = 1 someString = No Way THIRD id = 2 someString = Yah
I also saw that someone else posted a hybrid approach.
After playing with a few ideas, I have a rough solution that scratches my itch a bit better, but introduces it's own dangers. I don't believe that this is "the" good solution. We may need the Scala language to grow a bit more before we get a good robust approach that has all of the good stuff.
I basically just added iteration to the case class approach. This way it is easy to define the base class used by the values, and the compiler can see the objects as a complete set. Because I couldn't do it "right" I added some stuff to help me from doing some obvious dumb stuff.
I used lazy val initialization to help with defining the iteration order of the objects. Objects don't get created until they get referenced, so by defining the order, all of the objects in the order get initialized, and then the defineOrder function "defines" the enum. If another object is later referenced that was not included in the official order, it will complain. Likewise, if you try to do anything interesting without defining order, it will also complain.
There are parts of the approach that are a bit sloppy. I.e. the "values" list is a list of the items as the base "value" class, while the user has to define their own more specifically typed list "order" if needed. I tried to work around this, but the Java erasure and Scala manifests etc. just left me a bit bloodied before I gave up and stuck with the simple approach that worked well enough.
This is my first pass prototype. Seems to do most of what I wanted, but I was interested in feedback.
abstract class AltEnum{ private var orderDefined = false private var privValues:Option[List[Value]] = None lazy val values:List[Value] = { if( ! orderDefined ) throw new Error("Tried to access id without defining order") // this shouldn't be empty if orderDefined if( privValues.isEmpty ) throw new Error("Unexpected empty priv value") privValues.get } def defineOrder[T <: Value](list:List[T]):List[T]={ orderDefined = true var nextId = 0 list.foreach( (v)=>{ v.privId = nextId nextId += 1 v.id // cause id to init }) privValues = Some(list) list }
// look through list and find value with id def getById[T<:Value](id:Int, list:List[T]):Option[T]={ list.find((i)=>{i.id == id}) } // look through list and find value with id def getByName[T<:Value](name:String, list:List[T]):Option[T]={ list.find((i)=>{i.name == name}) }
protected class Value(){ if( orderDefined ) throw new Error("Created value or accessed value that was not in defined order") private[AltEnum] var privId:Int = -1 lazy val id = { if( ! orderDefined ) throw new Error("Tried to access id without defining order") privId } lazy val name = { if( ! orderDefined ) throw new Error("Tried to access name without defining order") this.toString } }}
Here is a sample usage:
object JunkEnum extends AltEnum{ sealed class Junk(val someString:String) extends Value
case object FIRST extends Junk("Wow") case object SECOND extends Junk("No Way") case object THIRD extends Junk("Yah")
val order = defineOrder(FIRST::SECOND::THIRD::Nil)
}
object Main2 { def main(args: Array[String]): Unit = { println("enum list :" + JunkEnum.order) import JunkEnum._ for( j <- order){ println(" "+j.name+" id = "+j.id+" someString = "+j.someString) } }}
Output:
enum list :List(FIRST, SECOND, THIRD) FIRST id = 0 someString = Wow SECOND id = 1 someString = No Way THIRD id = 2 someString = Yah