This page is no longer maintained — Please continue to the home page at www.scala-lang.org

"True" Enumerations for Scala

7 replies
Rüdiger Keller
Joined: 2010-01-24,
User offline. Last seen 42 years 45 weeks ago.

Hi list,

when I got up this morning, I suddenly had this vision of "true" Scala
enumeration support. ;-)

Probably not everybody agrees, but to me Scala's Enumeration is next
to useless, because I usually need to customize my enums. This is
easily done in Java, but unfortunately impossible with the Scala
Enumeration.

The usual answer to this is: use case objects and maybe add a sealed
base trait to the mix. And often that is really all you need. But
unfortunately sometimes you just need to enumerate your values / case
objects. Up to now I have done this "by hand", by adding a Int field
"ordinal" to my sealed base trait, with the proper values in my
objects, and also adding a companion object with a Seq of all the case
objects. But this is tedious and error prone, as it has to be done
manually.

So I had this idea: what if the compiler would do this for you? I
envisioned a marker trait Enum[T] with the methods

def values: IndexedSeq[T]
def ordinal(a : T): Int

Assume a sealed trait Base, then one would add an object that extends
Enum[Base] to the same source file. The compiler would fill in the
implementations of Enum's methods. values would return a Seq
containing all objects directly deriving from Base, in the order of
definition in the file. ordinal would give the index of the object in
the Seq returned by values.

Thinking about it, I would like it even better, if the objects
deriving from Base could be retrofitted by the compiler with a ordinal
method. Hmm, maybe that could be done by letting Base extend a trait
EnumBase that provides the ordinal method, and again letting the
compiler fill in the implementations. Non direct descendants of Base
could inherit a default implementation that returns zero or throws an
Exception.

If such a suggestion has been discussed before, please ignore my
pointless ramblings, else flame on!

Cheers,
Ruediger

Dan Shryock
Joined: 2010-01-07,
User offline. Last seen 42 years 45 weeks ago.
Re: "True" Enumerations for Scala

I've actually tried to accomplish something similar by only using
standard language features.

My assumption was that all possible enum values must be inner case
objects, but the problem that I ran into was an inability to eagerly
register my inner objects with my outer object. Without being able to
eagerly access the inner objects, there is no way to create a list of
the inner objects without explicitly listing them.

Below is an example using an imaginary trait called EagerObjects which
has a method called initializeObjects that causes all of the inner
objects to be eagerly referenced.

I've actually tried implementing my EagerObjects trait using
reflection, and it works for normal classes with inner objects, but
not for objects with inner objects. My vote here would be to have a
more general purpose reflection system for scala that would allow us
to easily find all objects and initialize them.

Dan

trait Enum extends EagerObjects{
private var nextId = -1
private val enumValues = new scala.collection.mutable.ListBuffer[Value]

sealed trait Value{
self:Product =>
enumValues += this
val id = {nextId += 1;nextId}
def name = self.productPrefix
}

lazy val values = enumValues.toList

initializeObjects()
}

object Enum{
def test(){
Planets.values foreach println
Planets.values map {_.id} foreach println
}
}

object SimpleEnum extends Enum{
case object A extends Value
case object B extends Value
case object C extends Value
}

object Planets extends Enum{
sealed abstract class Planet(val mass:Double, val radius:Double)
extends Value{
self:Product =>
val G = 6.67300E-11
val surfaceGravity = G * mass / (radius * radius)
def surfaceWeight(otherMass:Double) = otherMass * surfaceGravity
override def toString = name+"("+mass+","+radius+")"
}

case object Mercury extends Planet(3.303e+23, 2.4397e6)
case object Venus extends Planet(4.869e+24, 6.0518e6)
case object Earth extends Planet(5.976e+24, 6.37814e6)
case object Mars extends Planet(6.421e+23, 3.3972e6)
case object Jupiter extends Planet(1.9e+27, 7.1492e7)
case object Saturn extends Planet(5.688e+26, 6.0268e7)
case object Uranus extends Planet(8.686e+25, 2.5559e7)
case object Neptune extends Planet(1.024e+26, 2.4746e7)
}

JamesJ
Joined: 2010-01-24,
User offline. Last seen 42 years 45 weeks ago.
Re: "True" Enumerations for Scala

I have had the same observations, plus an encounter with the "null
object". Below is the post that I made a while back. (Although, I
did not get any feedback)

I'm still interested to know if Scala is supposed to let you access an
undefined (null) object under some cases (shown in example code
below).

+1 for providing somehow to have eager objects, so that we can at
least build our own Enums that provide:
- compile time checking (both Enum and Case class solutions provide this)
- general extensibility (only case class solutions)
- auto-id generation (only Enum)

I'm new to Scala, I Love how it has helped me be
efficiently expressive, but I can't believe that a language that
prides itself on less boilerplate does not provide an enum solution
that provides the three features listed above. Once I implemented the
prototype implementation below, it immediately let me clean up code
that was trying to map properties and methods to the builtin scala
enums. But, I still love Scala!

Thanks

James

--------------- Prev Post ---------------------
Hi,

To understand my question, I first have to give a short explanation.

I was unhappy with Scala Enums lack of extensibility, and didn't like
the case class solution because it didn't offer iteration.

I ended up creating a "least-pain" (for my programming style), where I
added ordering to a set of sealed case classes and built in a bit of
"at least catch the dumb stuff" implicit error checking.

This leads me to my second part of the subject, "Eager". The "object"
key word seems to be "lazy" by default, but this is not helpful in my
case. I want the objectness that implies that there is only one, but
I also want it all initialized at the same time is other stuff

My Question: Could we have a converse to lazy keyword, that makes
objects init early instead of waiting to be accessed? It could be
called, eager, !lazy or whatever.

Third part: I would never have guessed that I could get a null when
accessing objects, but I ran across it yesterday. Any insight would
be appreciated.
If I access order first, all is well, but if I touch a case object
first, then that case object is returned as null subsequently. I am
using 2.8

Thanks

JamesJ

------------------------------- Code that breaks stuff

package junk

import com.kybios.scalaUtil.AltEnum

object SampleEnum extends AltEnum{
sealed class SampleId(val someString:String) extends Value

case object FIRST extends SampleId("Wow")
case object SECOND extends SampleId("No Way")
case object THIRD extends SampleId("Yah")

val order = defineOrder(FIRST::SECOND::THIRD::Nil)

}

object Main {
def main(args: Array[String]): Unit = {
import SampleEnum._
//val tmp = FIRST // ****** Uncomment this statement to break
stuff **********
println("enum list :" + order)
for( j <- order){
println(" "+j.name+" id = "+j.id+" someString = "+j.someString)
}
}
}

------------------------------- My alternate enum solution utilized above
package com.kybios.scalaUtil

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
}
}
}

extempore
Joined: 2008-12-17,
User offline. Last seen 35 weeks 3 days ago.
Re: "True" Enumerations for Scala

On Thu, Nov 04, 2010 at 06:08:53PM -0600, JamesJ wrote:
> I'm still interested to know if Scala is supposed to let you access an
> undefined (null) object under some cases (shown in example code
> below).

You can always run into null by creating cycles in the init order. I
don't immediately see what's happening in your example, but I observe
that with a small change the problem goes away.

> object SampleEnum extends AltEnum{
> sealed class SampleId(val someString:String) extends Value
>
> case object FIRST extends SampleId("Wow")
> case object SECOND extends SampleId("No Way")
> case object THIRD extends SampleId("Yah")
>
> val order = defineOrder(FIRST::SECOND::THIRD::Nil)
> }

Instead write that as:

class MySampleEnum extends AltEnum {
sealed class SampleId(val someString:String) extends Value

case object FIRST extends SampleId("Wow")
case object SECOND extends SampleId("No Way")
case object THIRD extends SampleId("Yah")

val order = defineOrder(FIRST::SECOND::THIRD::Nil)
}

object SampleEnum extends MySampleEnum { }

And it no longer NPEs. If there's an initialization order bug in there
then I'd really like to know about it; an enumeration bug is of less
interest because I think there's a consensus we're returning to the
drawing board on enumeration anyway.

JamesJ
Joined: 2010-01-24,
User offline. Last seen 42 years 45 weeks ago.
Re: "True" Enumerations for Scala

Thanks, for the observation. However by introducing an extra class,
the namespace gets even more polluted, my workaround has been to just
make sure to access the order first as part of init to make sure it
works.

I still don't see why accessing an object inside of an hierarchy ought
to make all subsequent accesses of that object null?

On Thu, Nov 4, 2010 at 8:47 PM, Paul Phillips wrote:
> On Thu, Nov 04, 2010 at 06:08:53PM -0600, JamesJ wrote:
>> I'm still interested to know if Scala is supposed to let you access an
>> undefined (null) object under some cases (shown in example code
>> below).
>
> You can always run into null by creating cycles in the init order.  I
> don't immediately see what's happening in your example, but I observe
> that with a small change the problem goes away.
>
>> object SampleEnum extends AltEnum{
>>     sealed class SampleId(val someString:String) extends Value
>>
>>     case object FIRST extends SampleId("Wow")
>>     case object SECOND extends SampleId("No Way")
>>     case object THIRD extends SampleId("Yah")
>>
>>     val order = defineOrder(FIRST::SECOND::THIRD::Nil)
>> }
>
> Instead write that as:
>
> class MySampleEnum extends AltEnum {
>  sealed class SampleId(val someString:String) extends Value
>
>  case object FIRST extends SampleId("Wow")
>  case object SECOND extends SampleId("No Way")
>  case object THIRD extends SampleId("Yah")
>
>  val order = defineOrder(FIRST::SECOND::THIRD::Nil)
> }
>
> object SampleEnum extends MySampleEnum { }
>
> And it no longer NPEs.  If there's an initialization order bug in there
> then I'd really like to know about it; an enumeration bug is of less
> interest because I think there's a consensus we're returning to the
> drawing board on enumeration anyway.
>
> --
> Paul Phillips      | Adultery is the application of democracy to love.
> In Theory          |     -- H. L. Mencken
> Empiricist         |
> pp: i haul pills   |----------* http://www.improving.org/paulp/ *----------
>

Ken Scambler
Joined: 2009-11-07,
User offline. Last seen 42 years 45 weeks ago.
Re: "True" Enumerations for Scala
Try
lazy val order = ...

On 5 November 2010 14:39, JamesJ <james [at] kybios [dot] com> wrote:
Thanks, for the observation.  However by introducing an extra class,
the namespace gets even more polluted, my workaround has been to just
make sure to access the order first as part of init to make sure it
works.

I still don't see why accessing an object inside of an hierarchy ought
to make all subsequent accesses of that object null?


On Thu, Nov 4, 2010 at 8:47 PM, Paul Phillips <paulp [at] improving [dot] org> wrote:
> On Thu, Nov 04, 2010 at 06:08:53PM -0600, JamesJ wrote:
>> I'm still interested to know if Scala is supposed to let you access an
>> undefined (null) object under some cases (shown in example code
>> below).
>
> You can always run into null by creating cycles in the init order.  I
> don't immediately see what's happening in your example, but I observe
> that with a small change the problem goes away.
>
>> object SampleEnum extends AltEnum{
>>     sealed class SampleId(val someString:String) extends Value
>>
>>     case object FIRST extends SampleId("Wow")
>>     case object SECOND extends SampleId("No Way")
>>     case object THIRD extends SampleId("Yah")
>>
>>     val order = defineOrder(FIRST::SECOND::THIRD::Nil)
>> }
>
> Instead write that as:
>
> class MySampleEnum extends AltEnum {
>  sealed class SampleId(val someString:String) extends Value
>
>  case object FIRST extends SampleId("Wow")
>  case object SECOND extends SampleId("No Way")
>  case object THIRD extends SampleId("Yah")
>
>  val order = defineOrder(FIRST::SECOND::THIRD::Nil)
> }
>
> object SampleEnum extends MySampleEnum { }
>
> And it no longer NPEs.  If there's an initialization order bug in there
> then I'd really like to know about it; an enumeration bug is of less
> interest because I think there's a consensus we're returning to the
> drawing board on enumeration anyway.
>
> --
> Paul Phillips      | Adultery is the application of democracy to love.
> In Theory          |     -- H. L. Mencken
> Empiricist         |
> pp: i haul pills   |----------* http://www.improving.org/paulp/ *----------
>

Ruediger Keller
Joined: 2010-04-11,
User offline. Last seen 42 years 45 weeks ago.
Re: "True" Enumerations for Scala

Good to know that Enumerations are on the to-do-list.

Regards,
Ruediger

2010/11/5 Paul Phillips :
> On Thu, Nov 04, 2010 at 06:08:53PM -0600, JamesJ wrote:
>> I'm still interested to know if Scala is supposed to let you access an
>> undefined (null) object under some cases (shown in example code
>> below).
>
> You can always run into null by creating cycles in the init order.  I
> don't immediately see what's happening in your example, but I observe
> that with a small change the problem goes away.
>
>> object SampleEnum extends AltEnum{
>>     sealed class SampleId(val someString:String) extends Value
>>
>>     case object FIRST extends SampleId("Wow")
>>     case object SECOND extends SampleId("No Way")
>>     case object THIRD extends SampleId("Yah")
>>
>>     val order = defineOrder(FIRST::SECOND::THIRD::Nil)
>> }
>
> Instead write that as:
>
> class MySampleEnum extends AltEnum {
>  sealed class SampleId(val someString:String) extends Value
>
>  case object FIRST extends SampleId("Wow")
>  case object SECOND extends SampleId("No Way")
>  case object THIRD extends SampleId("Yah")
>
>  val order = defineOrder(FIRST::SECOND::THIRD::Nil)
> }
>
> object SampleEnum extends MySampleEnum { }
>
> And it no longer NPEs.  If there's an initialization order bug in there
> then I'd really like to know about it; an enumeration bug is of less
> interest because I think there's a consensus we're returning to the
> drawing board on enumeration anyway.
>
> --
> Paul Phillips      | Adultery is the application of democracy to love.
> In Theory          |     -- H. L. Mencken
> Empiricist         |
> pp: i haul pills   |----------* http://www.improving.org/paulp/ *----------
>

Naftoli Gugenheim
Joined: 2008-12-17,
User offline. Last seen 42 years 45 weeks ago.
Re: "True" Enumerations for Scala
Let's say you solve the reflection problem. How will you manage assigning stable ordinals to the case objects? The java reflection methods don't always return the methods in the same order for some reason (perhaps after different compilations, don't remember if it was the same version of scala). In any case you can't really on the order of case objects remaining the same. What if you want to insert one in between? It just occurred to me, I wonder if it's possible to take a slightly different, slightly more verbose approach.Define your case objects at top level (perhaps with a sealed base trait). Then you would write something like:
object Planets extends Enum/*[Planets]*/(Mercury, Venus, Earth /* ... */)
Then, Enum could provide all the standard features like iterating and numbering the values. In addition, since Planets is the companion object of the planets' supertype, Enum could define implicits that would automatically be available to members of the enum. So you could write Mercury.id (or perhaps the more unique enumId etc.) and it would actually call an implicit on Planets (via Enum). Also, maybe you could refer to the type of the enum as an enum type by using views or other implicit parameters. Something like this:
scala> class Enum[+T](values: T*) {  implicit def e[U>:T](v: U): Enum[U] = this }
object O {  implicit object Planet extends Enum[Planet](Earth,Mars)  trait Planet  case object Mars extends Planet  case object Earth extends Planet }
import O._
def n[T : Enum] = 0
defined class Enumdefined module Oimport O._n: [T](implicit evidence$2: Enum[T])Int
scala> n(Earth)res0: Int = 0



On Thu, Nov 4, 2010 at 1:58 PM, Dan Shryock <dan [dot] shryock [at] gmail [dot] com> wrote:
I've actually tried to accomplish something similar by only using
standard language features.

My assumption was that all possible enum values must be inner case
objects, but the problem that I ran into was an inability to eagerly
register my inner objects with my outer object.  Without being able to
eagerly access the inner objects, there is no way to create a list of
the inner objects without explicitly listing them.

Below is an example using an imaginary trait called EagerObjects which
has a method called initializeObjects that causes all of the inner
objects to be eagerly referenced.

I've actually tried implementing my EagerObjects trait using
reflection, and it works for normal classes with inner objects, but
not for objects with inner objects.  My vote here would be to have a
more general purpose reflection system for scala that would allow us
to easily find all objects and initialize them.

Dan


trait Enum extends EagerObjects{
 private var nextId = -1
 private val enumValues = new scala.collection.mutable.ListBuffer[Value]

 sealed trait Value{
   self:Product =>
   enumValues += this
   val id = {nextId += 1;nextId}
   def name = self.productPrefix
 }

 lazy val values = enumValues.toList

 initializeObjects()
}

object Enum{
 def test(){
   Planets.values foreach println
   Planets.values map {_.id} foreach println
 }
}

object SimpleEnum extends Enum{
 case object A extends Value
 case object B extends Value
 case object C extends Value
}

object Planets extends Enum{
 sealed abstract class Planet(val mass:Double, val radius:Double)
extends Value{
   self:Product =>
   val G = 6.67300E-11
   val surfaceGravity = G * mass / (radius * radius)
   def surfaceWeight(otherMass:Double) = otherMass * surfaceGravity
   override def toString = name+"("+mass+","+radius+")"
 }

 case object Mercury extends Planet(3.303e+23, 2.4397e6)
 case object Venus   extends Planet(4.869e+24, 6.0518e6)
 case object Earth   extends Planet(5.976e+24, 6.37814e6)
 case object Mars    extends Planet(6.421e+23, 3.3972e6)
 case object Jupiter extends Planet(1.9e+27,   7.1492e7)
 case object Saturn  extends Planet(5.688e+26, 6.0268e7)
 case object Uranus  extends Planet(8.686e+25, 2.5559e7)
 case object Neptune extends Planet(1.024e+26, 2.4746e7)
}

Copyright © 2012 École Polytechnique Fédérale de Lausanne (EPFL), Lausanne, Switzerland