Are there any best-practice guidelines on when to use case classes (or case objects) vs extending Enumeration in Scala?
They seem to offer some of the same benefits.
UPDATE: A new macro based solution has been created which is far superior to the solution I outline below. I strongly recommend using this new macro based solution. And it appears plans for Dotty will make this style of enum solution part of the language. Whoohoo!
Summary:
There are three basic patterns for attempting to reproduce the Java Enum within a Scala project. Two of the three patterns; directly using Java Enum and scala.Enumeration, are not capable of enabling Scala's exhaustive pattern matching. And the third one; "sealed trait + case object", does...but has JVM class/object initialization complications resulting in inconsistent ordinal index generation.
I have created a solution with two classes; Enumeration and EnumerationDecorated, located in this Gist. I didn't post the code into this thread as the file for Enumeration was quite large (+400 lines - contains lots of comments explaining implementation context).
Details:
The question you're asking is pretty general; "...when to use caseclassesobjects vs extending [scala.]Enumeration". And it turns out there are MANY possible answers, each answer depending on the subtleties of the specific project requirements you have. The answer can be reduced down to three basic patterns.
To start, let's make sure we are working from the same basic idea of what an enumeration is. Let's define an enumeration mostly in terms of the Enum provided as of Java 5 (1.5):
Enum, it would be nice to be able to explicitly leverage Scala's pattern matching exhaustiveness checking for an enumeration Next, let's look at boiled down versions of the three most common solution patterns posted:
A) Actually directly using Java Enum pattern (in a mixed Scala/Java project):
public enum ChessPiece {
KING('K', 0)
, QUEEN('Q', 9)
, BISHOP('B', 3)
, KNIGHT('N', 3)
, ROOK('R', 5)
, PAWN('P', 1)
;
private char character;
private int pointValue;
private ChessPiece(char character, int pointValue) {
this.character = character;
this.pointValue = pointValue;
}
public int getCharacter() {
return character;
}
public int getPointValue() {
return pointValue;
}
}
The following items from the enumeration definition are not available:
For my current projects, I don't have the benefit of taking the risks around the Scala/Java mixed project pathway. And even if I could choose to do a mixed project, item 7 is critical for allowing me to catch compile time issues if/when I either add/remove enumeration members, or am writing some new code to deal with existing enumeration members.
B) Using the "sealed trait + case objects" pattern:
sealed trait ChessPiece {def character: Char; def pointValue: Int}
object ChessPiece {
case object KING extends ChessPiece {val character = 'K'; val pointValue = 0}
case object QUEEN extends ChessPiece {val character = 'Q'; val pointValue = 9}
case object BISHOP extends ChessPiece {val character = 'B'; val pointValue = 3}
case object KNIGHT extends ChessPiece {val character = 'N'; val pointValue = 3}
case object ROOK extends ChessPiece {val character = 'R'; val pointValue = 5}
case object PAWN extends ChessPiece {val character = 'P'; val pointValue = 1}
}
The following items from the enumeration definition are not available:
It's arguable it really meets enumeration definition items 5 and 6. For 5, it's a stretch to claim it's efficient. For 6, it's not really easy to extend to hold additional associated singleton-ness data.
C) Using the scala.Enumeration pattern (inspired by this StackOverflow answer):
object ChessPiece extends Enumeration {
val KING = ChessPieceVal('K', 0)
val QUEEN = ChessPieceVal('Q', 9)
val BISHOP = ChessPieceVal('B', 3)
val KNIGHT = ChessPieceVal('N', 3)
val ROOK = ChessPieceVal('R', 5)
val PAWN = ChessPieceVal('P', 1)
protected case class ChessPieceVal(character: Char, pointValue: Int) extends super.Val()
implicit def convert(value: Value) = value.asInstanceOf[ChessPieceVal]
}
The following items from the enumeration definition are not available (happens to be identical to the list for directly using the Java Enum):
Again for my current projects, item 7 is critical for allowing me to catch compile time issues if/when I either add/remove enumeration members, or am writing some new code to deal with existing enumeration members.
So, given the above definition of an enumeration, none of the above three solutions work as they do not provide everything outlined in the enumeration definition above:
Each of these solutions can be eventually reworked/expanded/refactored to attempt to cover some of each one's missing requirements. However, neither the Java Enum nor the scala.Enumeration solutions can be sufficiently expanded to provide item 7. And for my own projects, this is one of the more compelling values of using a closed type within Scala. I strongly prefer compile time warnings/errors to indicate I have a gap/issue in my code as opposed to having to glean it out of a production runtime exception/failure.
In that regard, I set about working with the case object pathway to see if I could produce a solution which covered all of the enumeration definition above. The first challenge was to push through the core of the JVM class/object initialization issue (covered in detail in this StackOverflow post). And I was finally able to figure out a solution.
As my solution is two traits; Enumeration and EnumerationDecorated, and since the Enumeration trait is over +400 lines long (lots of comments explaining context), I am forgoing pasting it into this thread (which would make it stretch down the page considerbly). For details, please jump directly to the Gist.
Here's what the solution ends up looking like using the same data idea as above (fully commented version available here) and implemented in EnumerationDecorated.
import scala.reflect.runtime.universe.{TypeTag,typeTag}
import org.public_domain.scala.utils.EnumerationDecorated
object ChessPiecesEnhancedDecorated extends EnumerationDecorated {
case object KING extends Member
case object QUEEN extends Member
case object BISHOP extends Member
case object KNIGHT extends Member
case object ROOK extends Member
case object PAWN extends Member
val decorationOrderedSet: List[Decoration] =
List(
Decoration(KING, 'K', 0)
, Decoration(QUEEN, 'Q', 9)
, Decoration(BISHOP, 'B', 3)
, Decoration(KNIGHT, 'N', 3)
, Decoration(ROOK, 'R', 5)
, Decoration(PAWN, 'P', 1)
)
final case class Decoration private[ChessPiecesEnhancedDecorated] (member: Member, char: Char, pointValue: Int) extends DecorationBase {
val description: String = member.name.toLowerCase.capitalize
}
override def typeTagMember: TypeTag[_] = typeTag[Member]
sealed trait Member extends MemberDecorated
}
This is an example usage of a new pair of enumeration traits I created (located in this Gist) to implement all of the capabilities desired and outlined in the enumeration definition.
One concern expressed is that the enumeration member names must be repeated (decorationOrderedSet in the example above). While I did minimize it down to a single repetition, I couldn't see how to make it even less due to two issues:
getClass.getDeclaredClasses has an undefined order (and it is quite unlikely to be in the same order as the case object declarations in the source code)Given these two issues, I had to give up trying to generate an implied ordering and had to explicitly require the client define and declare it with some sort of ordered set notion. As the Scala collections do not have an insert ordered set implementation, the best I could do was use a List and then runtime check that it was truly a set. It's not how I would have preferred to have achieved this.
And given the design required this second list/set ordering val, given the ChessPiecesEnhancedDecorated example above, it was possible to add case object PAWN2 extends Member and then forget to add Decoration(PAWN2,'P2', 2) to decorationOrderedSet. So, there is a runtime check to verify that the list is not only a set, but contains ALL of the case objects which extend the sealed trait Member. That was a special form of reflection/macro hell to work through.
Please leave comments and/or feedback on the Gist.