public
Last active

DIY Scala Enums (with optional exhaustiveness checking)

  • Download Gist
ScalaEnum.scala
Scala
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
trait Enum { //DIY enum type
import java.util.concurrent.atomic.AtomicReference //Concurrency paranoia
 
type EnumVal <: Value //This is a type that needs to be found in the implementing class
 
private val _values = new AtomicReference(Vector[EnumVal]()) //Stores our enum values
 
//Adds an EnumVal to our storage, uses CCAS to make sure it's thread safe, returns the ordinal
private final def addEnumVal(newVal: EnumVal): Int = { import _values.{get, compareAndSet => CAS}
val oldVec = get
val newVec = oldVec :+ newVal
if((get eq oldVec) && CAS(oldVec, newVec)) newVec.indexWhere(_ eq newVal) else addEnumVal(newVal)
}
 
def values: Vector[EnumVal] = _values.get //Here you can get all the enums that exist for this type
 
//This is the trait that we need to extend our EnumVal type with, it does the book-keeping for us
protected trait Value { self: EnumVal => //Enforce that no one mixes in Value in a non-EnumVal type
final val ordinal = addEnumVal(this) //Adds the EnumVal and returns the ordinal
 
def name: String //All enum values should have a name
 
override def toString = name //And that name is used for the toString operation
override def equals(other: Any) = this eq other.asInstanceOf[AnyRef]
override def hashCode = 31 * (this.getClass.## + name.## + ordinal)
}
}
 
//And here's how to use it, if you want compiler exhaustiveness checking
object Foos extends Enum {
sealed trait EnumVal extends Value /*{ you can define your own methods etc here }*/
 
val F = new EnumVal { val name = "F" }
val X = new EnumVal { val name = "X" }
}
 
/**
scala> Foos.values.find(_.name == "F")
res3: Option[Foos.EnumVal] = Some(F)
 
scala> Foos.X.ordinal
res4: Int = 1
 
scala> def doSmth(foo: Foos.EnumVal) = foo match {
case Foos.X => println("pigdog")
}
 
<console>:10: warning: match is not exhaustive!
missing combination $anon$1
missing combination $anon$2
 
scala> def doSmth(foo: Foos.EnumVal) = foo match {
case Foos.X => println("pigdog")
case Foos.F => println("dogpig")
}
doSmth: (foo: Foos.EnumVal)Unit
**/
 
//But if you don't care about getting exhaustiveness warnings, you can do:
 
object Foos extends Enum {
case class EnumVal private[Foos](name: String) extends Value /* { you can define your own methods and stuff here } */
 
val F = EnumVal("F")
val X = EnumVal("X")
}
 
/**
Which is a bit less boilerplatey.
 
 
Cheers,
**/

//So if I want an external base Type class I would do it like so:

sealed trait AttrType extends AttrType.EnumVal /{ you can define your own methods etc here }/

/** these types MUST be supported by forms for capture, not necessarily by displays /
object AttrType extends Enum {
trait EnumVal extends Value /
{ you can define your own methods etc here }*/

val STRING = new AttrType { val name = "STRING" }
val MEMO = new AttrType { val name = "MEMO" }
val SCRIPT = new AttrType { val name = "SCRIPT" }
val INT = new AttrType { val name = "INT" }
val FLOAT = new AttrType { val name = "FLOAT" }
val DATE = new AttrType { val name = "DATE" }
val DEFAULT = new AttrType { val name = "DEFAULT" }
val ENUM = new AttrType { val name = "ENUM" }
/** ENUM not supported yet */
}

trait NameValuePairs [+V] extends collection.mutable.Map[String,V] {
val types = new collection.mutable.HashMapString,AttrType

def set (name:String, value:V, ttype:AttrType) {
this put (name, value)
types put (name, ttype)
}
}

I do not understand "NameValuePairs", what is it needed for?
(aside from being thread-unsafe)

You can use AttrType.values as you like.

That was meant as a usage sample - mainly for the set method which takes an AttrType. The example indeed is wrong, makes more sense less generic:

trait NameValuePairs extends collection.mutable.Map[String, AnyRef] {
val types = new collection.mutable.HashMapString,AttrType

def set (name:String, value:AnyRef, ttype:AttrType) {
this put (name, value)
types put (name, ttype)
}
}

This wants to be a generic model for a set of objects with optional types.

Indeed, you're right, it can simply be Map[String, (AnyRef, AttrType)]

trait NameValuePairs extends collection.mutable.Map[String, (AnyRef, Option[AttrType])] {
def set (name:String, value:AnyRef, ttype:AttrType = null) {
this put (name, (value, Option(ttype))) // forgive the nulls - I'm lazy
}

def types = this.values.map (_._2)
//...
}

Is that what you meant?

Cheers,
Razie

-----Original Message-----
From: viktorklang [mailto:reply@reply.github.com]
Sent: July-07-11 11:46 AM
To: razie@razie.com
Subject: Re: gist gist: 1057513

I do not understand "NameValuePairs", what is it needed for?
(aside from being thread-unsafe)

You can use AttrType.values as you like.

Reply to this email directly or view it on GitHub:
https://gist.github.com/1057513

I still don't understand the point of: val types = new collection.mutable.HashMap ...

It holds the optional types, for those attributes/fields where types were specified. I think the gisthub mangled the square brackets.

Here's the current functional version which I had in mind and tried to simplify to have an example: https://gist.github.com/1070245

I guess this should be obvious, but can you show and example which has an extra method on the usage?

Thanks

Hey Ray,

here's an example:

scala> object Foos extends Enum {
| case class EnumVal privateFoos extends Value {
| def bippy(i: Int) = name + i
| }
|
| val F = EnumVal("F")
| val X = EnumVal("X")
| }
defined module Foos

scala>

scala> Foos.F.bippy(3)
res1: java.lang.String = F3

scala>

Thanks Viktor. Having seen it I feel dumb ;-)

One thing that the Java enums suffer from is their lack of extensibility. I'm gonna take it and make something like this http://blogs.oracle.com/darcy/entry/enums_and_mixins

The sky is the limit with Enum and traits.
After hearing the feedback on the latest scalatypes podcast I'll talk to the
scala team if it could be in scala 2.10

Sarcasm? ;)
On Jul 27, 2011 10:38 PM, "raymcdermott" <
reply@reply.github.com>
wrote:

Good luck.

Reply to this email directly or view it on GitHub:
https://gist.github.com/1057513

Paranoia? ;-)

It's an improvement but the way the discussion went on the user group you
may have a few arguments about priorities on the dev group. It's better
than the current condition so it gets my vote.

On 27 July 2011 23:12, viktorklang <
reply@reply.github.com>wrote:

Sarcasm? ;)
On Jul 27, 2011 10:38 PM, "raymcdermott" <
reply@reply.github.com>
wrote:

Good luck.

Reply to this email directly or view it on GitHub:
https://gist.github.com/1057513

Reply to this email directly or view it on GitHub:
https://gist.github.com/1057513

I think it could be fairly easy to deprecate Enumeration and add Enum,
then gradually convert internal on an as-needed basis.

Sounds like a plan... go for it!

Oh, by the way... and now I really am showing my lack of Scala education / grokness...

I want to do something like this:

trait SomethingWithSize {
def size: Int
}

trait EnumWithSize extends Enum with SomethingWithSize

and then use it like this:

def convertStringToMap(inputString: String, mappings: EnumWithSize) {
val mappingsExpectedLength = mappings.values.map(_.size).sum
// more stuff...
}

object HeaderMap extends EnumWithSize {
case class EnumVal privateHeaderMap extends Value with EnumWithSize
val HEADER_EL = EnumVal("HEADER_EL", 1)
val HEADER_RESERVED1 = EnumVal("HEADER_RESERVED1", 5)
val HEADER_RESERVED2 = EnumVal("HEADER_RESERVED2", 2)
val HEADER_MESSAGE_TYPE = EnumVal("HEADER_MESSAGE_TYPE", 4)
....
}

But oops! And now I'm getting my knickers in a twist...
:10: error: object creation impossible, since method size in trait SomethingWithSize of type => Int is not defined

I hope you can see my intention from this jibberish.... I want to extend Enum so that I can have something with a size...

Any tips welcome!

Cheers

ray

trait SomethingWithSize {
def size: Int
}

trait EnumWithSize extends Enum { type EnumVal <: Value with
SomethingWithSize }

object F extends EnumWithSize {
case class EnumVal privateF extends Value with
SomethingWithSize
val x = EnumVal("foo", 3)
val y = EnumVal("bar", 4)
}

def foo(smth: EnumWithSize) = smth.values.map(_.size)

scala> foo(F)
res3: scala.collection.immutable.Vector[Int] = Vector(3, 4)

I was close ;-) Thanks Viktor, I owe you a beer which I will happily provide if you are at Devoxx again this year...

I made one further change, as the code as it stands does not allow the names to be accessed in a generic fashion ;-)

trait SomethingWithANameAndASize {
    def size: Int
    def name: String
}

trait EnumWithSize extends Enum {
    type EnumVal <: Value with SomethingWithANameAndASize
}

object TqsHeaderMap extends EnumWithSize {
    case class EnumVal private[TqsHeaderMap](name: String, size: Int) extends Value with SomethingWithANameAndASize
    val HEADER_EL = EnumVal("HEADER_EL", 1)
    val HEADER_RESERVED1 = EnumVal("HEADER_RESERVED1", 5)
    val HEADER_RESERVED2 = EnumVal("HEADER_RESERVED2", 2)
    val HEADER_MESSAGE_TYPE = EnumVal("HEADER_MESSAGE_TYPE", 4)
}

def convertStringToMap(inputString: String, mappings: EnumWithSize): Map[SomethingWithANameAndASize, String] = {
    val mappingsExpectedLength = mappings.values.map(_.size).sum
    if (inputString.length != mappingsExpectedLength)
        throw new IllegalArgumentException("Actual length " + inputString.length + " not equal to expected record size of " + mappingsExpectedLength)

    var end = 0
    (mappings.values.map {
        field =>
        val start = end
        end += field.size
        (field, inputString.substring(start, end))
    }).toMap
}

class TqsMappingTests extends Spec with ShouldMatchers {

    describe("record header mapping") {
    val input = "L       0030"

    describe("with a string \"" + input + "\" mapped to 4 fields") {

            val result = new RecordToMap().convertStringToMap(input, TqsHeaderMap)

            it("should be 4 items in length") {
                result.size should be(4)
            }

            it("should have a head of L") {
                result.values.head should be("L")
            }

            it("should be able to access a random value using the Enum symbolic name") {
               result.get(TqsHeaderMap.HEADER_MESSAGE_TYPE) should equal(Some("0030"))
            }
        }
    }
}

And I am done! Thanks for the great support Viktor.

What?

scala> F.x.name
res0: String = foo

scala> def foo(e: EnumWithSize) = e.values.map(_.name)
foo: (e: EnumWithSize)scala.collection.immutable.Vector[String]

I should explain further.... If my function signature is:

def convertStringToMap(inputString: String, mappings: EnumWithSize): Map[EnumWithSize, String]

Then the caller cannot access _.name in the values returned in the map without including the EnumVal type of TqsHeader.... and that's not good.

And talking of surprises... I have done some tests on the real code that I need to write and observe that the order of the values is not always aligned with the order of the declaration of the enumeration. For example if I parse a record of 500 characters into 50 parts the head is not always the first element declared. It might not be a bug but it surprised me. I remember you tweeted a while ago about feeling bad about opening tickets on source projects. Imagine how bad I feel, bitching about a GIST ;-)

I find that very strange, can you add a println in the constructor of your
EnumVal to verify if it's a bug in the init order or a bug in Enum itself?

Ha! It is me being daft... I am actually using the values from the map that I return in my function. Panic over ;-)

On Fri, Jul 29, 2011 at 11:52 AM, raymcdermott <
reply@reply.github.com>wrote:

Ha! It is me being daft... I am actually using the values from the map that
I return in my function. Panic over ;-)

phew :D

Reply to this email directly or view it on GitHub:
https://gist.github.com/1057513

Viktor Klang

Akka Tech Lead
Typesafe http://www.typesafe.com/ - Enterprise-Grade Scala from the
Experts

Twitter: @viktorklang

Unfortunately, anonymous classes extending a sealed trait don't seem to work with exhaustiveness checks:

  val F = new EnumVal { val name = "F" }
  val X = new EnumVal { val name = "X" }

then

scala> def doSmth(foo: Foos.EnumVal) = foo match {
case Foos.X => println("pigdog")
case Foos.F => println("dogpig")
}

<console>:10: warning: match is not exhaustive!
missing combination $anon$1
missing combination $anon$2

Actually, even in your example where one enum value is missing, the warning message you listed shows both as a missing combination.

I tried this with 2.9.1, 2.9.0-1 and the 2.8 which runs on simplyscala.com

Sounds like a compiler bug.
Have you've opened a ticket?

Cheers,

Was just trying to confirm that it's a compiler bug and not by design. In order to do this, I wanted to find the version where it was supposed to work, but so far without much success.

I did manage to get the REPL on simplyscala.com to show no warnings whatsoever for both the missing combination and for all enum values, so I thought it could be a class previously loaded which interfered. Maybe the same happened when you tried this? There's some evidence that something was already wrong if you've pasted the console session verbatim.

So I'm wondering- did it really work when you tried it or was it just a glitch of the REPL? I've already calculated that at the time you posted this, the stable version was 2.9.0-1, and it doesn't work there.

That's a while back, I'm pretty sure it was working back then, but it
could've been a REPL glitch or something.
OTOH it ought to work, so IMHO it's bug if it isn't.

Even if it never worked, we should file it as a feature request because I want my statically checked enumeration matching!

IT should most definitely work

A simple addition which I find useful:

def apply(i:Int) = vectors(i)

This allows code like Foos(0) to lookup an enum instead of Foos.values(0)

A noob question. I would like to extend an Enum

e.g.

object Foos extends Enum {
    case class EnumVal private[Foos](name: Int) extends Value 

    val A = EnumVal(1)
    val B = EnumVal(2)
}

object AnotherFoos extends Foos{
    val C = EnumVa(3)
}

so I could call AnotherFoos(1).

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.