Skip to content

Instantly share code, notes, and snippets.

@viktorklang
Created June 30, 2011 23:12
Show Gist options
  • Star 66 You must be signed in to star a gist
  • Fork 11 You must be signed in to fork a gist
  • Save viktorklang/1057513 to your computer and use it in GitHub Desktop.
Save viktorklang/1057513 to your computer and use it in GitHub Desktop.
DIY Scala Enums (with optional exhaustiveness checking)
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,
**/
@viktorklang
Copy link
Author

Hey Ray,

here's an example:

scala> object Foos extends Enum {
| case class EnumVal private[Foos](name: String) 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>

@raymcdermott
Copy link

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

@viktorklang
Copy link
Author

viktorklang commented Jul 27, 2011 via email

@raymcdermott
Copy link

Good luck.

@viktorklang
Copy link
Author

viktorklang commented Jul 27, 2011 via email

@raymcdermott
Copy link

raymcdermott commented Jul 27, 2011 via email

@viktorklang
Copy link
Author

viktorklang commented Jul 28, 2011 via email

@raymcdermott
Copy link

Sounds like a plan... go for it!

@raymcdermott
Copy link

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 private[HeaderMap](name: String, size: Int) 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

@viktorklang
Copy link
Author

viktorklang commented Jul 28, 2011 via email

@raymcdermott
Copy link

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

@raymcdermott
Copy link

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.

@viktorklang
Copy link
Author

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]

@raymcdermott
Copy link

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.

@raymcdermott
Copy link

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

@viktorklang
Copy link
Author

viktorklang commented Jul 29, 2011 via email

@raymcdermott
Copy link

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

@viktorklang
Copy link
Author

viktorklang commented Jul 29, 2011 via email

@vdichev
Copy link

vdichev commented Dec 19, 2011

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

@viktorklang
Copy link
Author

viktorklang commented Dec 19, 2011 via email

@vdichev
Copy link

vdichev commented Dec 19, 2011

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.

@viktorklang
Copy link
Author

viktorklang commented Dec 19, 2011 via email

@vdichev
Copy link

vdichev commented Dec 19, 2011

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

@viktorklang
Copy link
Author

viktorklang commented Dec 19, 2011 via email

@hrj
Copy link

hrj commented Jun 8, 2012

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)

@sweepy84
Copy link

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
Copy link

giabao commented Jul 30, 2014

@sweepy84 - we can NOT extends an object

@chaotic3quilibrium
Copy link

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

@chaotic3quilibrium
Copy link

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.

@GitsMcGee
Copy link

@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