Created

Embed URL

HTTPS clone URL

SSH clone URL

You can clone with HTTPS or SSH.

Download Gist

DIY Scala Enums (with optional exhaustiveness checking)

View ScalaEnum.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,
**/
razie commented

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

Owner

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

You can use AttrType.values as you like.

razie commented
Owner

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

razie commented

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

Owner

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

Owner

Good luck.

Owner
Owner

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

Owner

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.

Owner

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 ;-)

Owner

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

Owner

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

Owner

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.

Owner

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

Owner
hrj commented

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).

giabao commented

@sweepy84 - we can NOT extends an object

I have copied this verbatim in Scala IDE 3.0.4 with Scala 2.11.2. And I am getting an exhaustive check warning for the code snippet Victor Klang shows as part of his REPL session; i.e. this one:
def doSmth(foo: Foos.EnumVal) = foo match {
case Foos.F => println("dogpig")
case Foos.X => println("pigdog")
}
Has something changed in Scala between the time Victor wrote this (mid 2011) and today (mid 2014)?

Summary:
I can’t get Viktor’s code to do pattern matcher exhaustiveness checking. I figured out how to do it using case objects while also eliminating the class/object initialization ordering issues (which cause the ordinals to not consistently be assigned to the same case object instances). Here’s the tinyurl (longer one below): http://goo.gl/zlCs61

Details:
I have so longed for Java’s Enum in Scala (without having to created Scala/Java mixed project and resorting to using Java’s Enum). As I began a large project several months ago, I decided to try and get the Scala enumeration problem solved again. I was very hopeful someone had created a DIY solution...which worked. When I saw Viktor’s come up in my Google search, I was pretty excited, but...

I have tried extensively to get Viktor's code above to work. I have not found a way to get it to provide pattern matching exhaustiveness checking. And I have tried LOTS of variations.

So, I decided to to move to using case objects (as opposed to a series of val-s) which I knew worked with the pattern matcher’s exhaustiveness checker. I got right to the edge of something that worked exactly like I wanted...when I exposed an issue with class/object initialization ordering, which Rex Kerr and I worked through on StackOverflow (a couple of years ago):
http://stackoverflow.com/questions/14947179/using-a-custom-enum-in-the-scala-worksheet-i-am-receiving-an-error-java-lang-ex

ARGH!!!!!!!!

So, I have now invested a considerable amount of time wrestling many Scala and JVM demons to hack my way to runtime victory. Here is a link to org.public_domain.scala.utils.Enumeration which provides the closest analog to all of the benefits I got from Java’s Enum:

Final solution:
https://gist.github.com/chaotic3quilibrium/57add1cd762eb90f6b24#file-org-public_domain-scala-utils-enumeration-scala

Example usages:
Simple: https://gist.github.com/chaotic3quilibrium/57add1cd762eb90f6b24#file-org-public_domain-chess-chesspiecessimplest-scala

Enhanced:
https://gist.github.com/chaotic3quilibrium/57add1cd762eb90f6b24#file-org-public_domain-chess-chesspiecesenhanced-scala

I would appreciate any feedback you might have on this.

Thank you.

@chaotic3quilibrium - Wow. That code is pretty intense! Any thoughts on releasing it as a library?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.