Skip to content

Instantly share code, notes, and snippets.

@chaotic3quilibrium
Last active September 13, 2020 22:20
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save chaotic3quilibrium/57add1cd762eb90f6b24 to your computer and use it in GitHub Desktop.
Save chaotic3quilibrium/57add1cd762eb90f6b24 to your computer and use it in GitHub Desktop.
DIY Scala Enumeration (closest possible Java Enum equivalent with guaranteed pattern matching exhaustiveness checking)
README.txt - DIY Scala Enumeration
Copyright (C) 2014-2016 Jim O'Flaherty
Overview:
Provide in Scala the closest equivalent to Java Enum
- includes decorating each declared Enum member with extended information
- guarantees pattern matching exhaustiveness checking
- this is not available with scala.Enumeration
ScalaOlio library (GPLv3) which contains more up-to-date versions of both `org.scalaolio.util.Enumeration` and `org.scalaolio.util.EnumerationDecorated`:
http://scalaolio.org/
https://github.com/chaotic3quilibrium/scala-olio
StackOverflow answer covering entire Scala enumeration options domain:
http://stackoverflow.com/a/25923651/501113
Gist Files:
Enumeration.scala - Baseline to extend for any implementation
ChessPiecesSimplest.scala - Shows the absolute minimum required to generate an implementation of Enumeration
WorkSheetChessPiecesSimplestTest.scala - Simple test cases for validating ChessPiecesSimplest
ChessPiecesEnhanced.scala - Shows a more elaborate method to extend Enumeration to associate data to a Member
WorkSheetChessPiecesEnhancedTest.scala - Simple test cases for validating ChessPiecesEnhanced
EnumerationDecorated.scala - Extends Enumeration to enable safely adding a non-Serialized singleton-ness Decoration to each declared member
ChessPiecesEnhancedDecorated.scala - Shows how ChessPiecesEnhanced.scala can be simplified by using this Enumeration extension
WorkSheetChessPiecesEnhancedDecoratedTest.scala - Simple test cases for validating ChessPiecesEnhancedDecorated
Most terse usage (see example files for more expansive details):
object ChessPiecesSimplest extends Enumeration {
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
protected val orderedSet: List[Member] = List(KING, QUEEN, BISHOP, KNIGHT, ROOK, PAWN)
sealed trait Member extends MemberBase
override def typeTagMember: TypeTag[_] = typeTag[Member]
}
--------------------
Details:
Below is a shorter version of a more thorough answer I posted on StackOverflow:
http://stackoverflow.com/a/25923651/501113
Ever since moving to Scala, I have longed to have the Scala-ized equivalent of Java's Enum. This meant having the following benefits:
A) Not requiring a Scala/Java mixed project just to use Java's Enum
B) Minimize the amount of overhead/boilerplate required to declare an Enumeration
C) Minimize the amount of overhead/boilerplate required to associate extended attributes to an Enumeration
D) Leverage the more sophisticated Scala compiler's ability to perform pattern matching exhaustiveness checking
E) If there was any requirement to "duplicate" the enumeration members, runtime catch any discrepancies
F) Deal with the many non-obvious JVM class/object initialization issues which have undermined other elegant solutions
After looking at scala.Enumeration and discovering it did not work for D, I searched for solutions that might optimally work. I came across the following StackOverflow solution:
http://stackoverflow.com/a/8620085/501113
The comments on this solution end up chasing down some weird JVM class/object initialization issues which are detailed here:
http://stackoverflow.com/questions/14947179/using-a-custom-enum-in-the-scala-worksheet-i-am-receiving-an-error-java-lang-ex
This solution pathway didn't work out (see final comment on original post). It just wasn't reasonable to require a client access "the correct case object" to guarantee proper Enumeration initialization. And without that, the same case object might received a different ordinal on any future run.
Pause two years. I need to have a reasonable implementation of the above benefits. I searched hoping something had turned up. I became quite excited when I came across Viktor Klang's (attempted) solution to the problem:
https://gist.github.com/viktorklang/1057513
Well, after spending considerable time trying to get Viktor's solution to work (in 2.11.2) with pattern matching exhaustiveness checking, I finally punted and decided to tackle this problem again. And close to 50 hours and a bazillion tangents later, this solution emerged. And it now offers all of the above benefits.
//ChessPiecesEnhanced.scala
// - Example usage of org.public_domain.scala.util.Enumeration
//Copyright (C) 2014-2016 Jim O'Flaherty
//
//
//This is free and unencumbered software released into the public domain.
//
//Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software,
//either in source code form or as a compiled binary, for any purpose, commercial or
//non-commercial, and by any means.
//
//In jurisdictions that recognize copyright laws, the author or authors of this software
//dedicate any and all copyright interest in the software to the public domain. We make this
//dedication for the benefit of the public at large and to the detriment of our heirs and
//successors. We intend this dedication to be an overt act of relinquishment in perpetuity
//of all present and future rights to this software under copyright law.
//
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
//INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
//PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES
//OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT
//OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
//For more information, please refer to <http://unlicense.org>
//
//If you would like to obtain a custom/different/commercial license for this, please send an
//email with your request to <jim.oflaherty.jr@gmail.com>.
package org.public_domain.chess
import scala.reflect.runtime.universe.{TypeTag,typeTag}
import org.public_domain.scala.utils.Enumeration
object ChessPiecesEnhanced extends Enumeration {
//A. Enumerated values
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
//B. from Enumeration [item B], required type extension
sealed trait Member extends MemberBase {
//due to the unpredictable nature of object initialization, avoid
//using val anywhere within this trait (use only def and lazy val)
//if val must be used, it must not depend up on any aspect of the initialization of
// this Member instance, the enclosing base Enumeration object or it's derivation (within which
// this definition sits, which in this example is ChessPiecesEnhanced)
//B.1 Adding extended information via a def to avoid serialization overhead
def attributes: Attributes = attributesByMember(this)
//change to lazy val only if serialization isn't important and you *know* Serialization will
// never be important (which is a very difficult assertion to make for the entire future use
// of this)
}
//C. from Enumeration [item L]
override def typeTagMember: TypeTag[_] = typeTag[Member]
//D. Safe place to extend any/all needed data for a specific Member; i.e use all the vals you like
sealed case class Attributes private[ChessPiecesEnhanced] (member: Member, char: Char, pointValue: Int) {
val description: String = member.name.toLowerCase.capitalize
}
//E. Defining the associated data
val attributesList: List[Attributes] =
List(
Attributes(KING, 'K', 0)
, Attributes(QUEEN, 'Q', 9)
, Attributes(BISHOP, 'B', 3)
, Attributes(KNIGHT, 'N', 3)
, Attributes(ROOK, 'R', 5)
, Attributes(PAWN, 'P', 1)
)
//F. Generate the association between the specific Attributes instance and its member
// This is used by the Member trait [item B] above
val attributesByMember: Map[Member, Attributes] =
attributesList.map(attributes => (attributes.member, attributes)).toMap
//G. From Enumeration [item M], required ordering of case objects
// it's pushed way down here to enable an interation of attributeList to avoid having to
// specify the members a third time
protected val orderedSet: List[Member] = attributesList.map(_.member)
//because object/class initialization ordering is not predictable
//if client happens to access ChessPiecesSimplest.ROOK first, the initialization order is...
//object ChessPiecesSimplest, complete ROOK and then initialize KING, QUEEN, BISHOP, KNIGHT PAWN
//because ChessPieces.getClass.getDeclaredClasses order of returned classes is not predictable
//H. from Enumeration [item N], optional ordinal assignment
override def ordinalStart = 5
override def ordinalStep = 5
}
//
//I. Simple test for exhaustive pattern matching
// - just comment out any of the case entries
object ChessPiecesEnhancedTestExhaustivePatternMatch {
def stringMe(member: ChessPiecesEnhanced.Member): String =
member match {
case ChessPiecesEnhanced.KING => "Of the World"
case ChessPiecesEnhanced.QUEEN => "Of the World"
case ChessPiecesEnhanced.BISHOP => "Of the Church"
case ChessPiecesEnhanced.KNIGHT => "Of the Church"
case ChessPiecesEnhanced.ROOK => "Of the Castle"
case ChessPiecesEnhanced.PAWN => "Of the Field"
}
}
//ChessPiecesEnhancedDecorated.scala
// - Example usage of org.public_domain.scala.util.Enumeration
//Copyright (C) 2014-2016 Jim O'Flaherty
//
//
//This is free and unencumbered software released into the public domain.
//
//Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software,
//either in source code form or as a compiled binary, for any purpose, commercial or
//non-commercial, and by any means.
//
//In jurisdictions that recognize copyright laws, the author or authors of this software
//dedicate any and all copyright interest in the software to the public domain. We make this
//dedication for the benefit of the public at large and to the detriment of our heirs and
//successors. We intend this dedication to be an overt act of relinquishment in perpetuity
//of all present and future rights to this software under copyright law.
//
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
//INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
//PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES
//OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT
//OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
//For more information, please refer to <http://unlicense.org>
//
//If you would like to obtain a custom/different/commercial license for this, please send an
//email with your request to <jim.oflaherty.jr@gmail.com>.
package org.public_domain.chess
import scala.reflect.runtime.universe.{TypeTag,typeTag}
import org.public_domain.scala.utils.EnumerationDecorated
object ChessPiecesEnhancedDecorated extends EnumerationDecorated {
//A. Enumerated values
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
//B. Defining the associated data
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)
)
//C. Safe place to extend any/all needed data for a specific Member; i.e use all the vals you like
final case class Decoration private[ChessPiecesEnhancedDecorated] (member: Member, char: Char, pointValue: Int) extends DecorationBase {
val description: String = member.name.toLowerCase.capitalize
}
//D. from Enumeration [item L]
override def typeTagMember: TypeTag[_] = typeTag[Member]
//E. from EnumerationDecoration [item C], required type extension
sealed trait Member extends MemberDecorated
}
//
//F. Simple test for exhaustive pattern matching
// - just comment out any of the case entries
object ChessPiecesEnhancedDecoratedTestExhaustivePatternMatch {
def stringMe(member: ChessPiecesEnhancedDecorated.Member): String =
member match {
case ChessPiecesEnhancedDecorated.KING => "Of the World"
case ChessPiecesEnhancedDecorated.QUEEN => "Of the World"
case ChessPiecesEnhancedDecorated.BISHOP => "Of the Church"
case ChessPiecesEnhancedDecorated.KNIGHT => "Of the Church"
case ChessPiecesEnhancedDecorated.ROOK => "Of the Castle"
case ChessPiecesEnhancedDecorated.PAWN => "Of the Field"
}
}
//ChessPiecesSimplest.scala
// - Example usage of org.public_domain.scala.util.Enumeration
//Copyright (C) 2014-2016 Jim O'Flaherty
//
//
//This is free and unencumbered software released into the public domain.
//
//Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software,
//either in source code form or as a compiled binary, for any purpose, commercial or
//non-commercial, and by any means.
//
//In jurisdictions that recognize copyright laws, the author or authors of this software
//dedicate any and all copyright interest in the software to the public domain. We make this
//dedication for the benefit of the public at large and to the detriment of our heirs and
//successors. We intend this dedication to be an overt act of relinquishment in perpetuity
//of all present and future rights to this software under copyright law.
//
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
//INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
//PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES
//OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT
//OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
//For more information, please refer to <http://unlicense.org>
//
//If you would like to obtain a custom/different/commercial license for this, please send an
//email with your request to <jim.oflaherty.jr@gmail.com>.
package org.public_domain.chess
import scala.reflect.runtime.universe.{TypeTag,typeTag}
import org.public_domain.scala.utils.Enumeration
object ChessPiecesSimplest extends Enumeration {
//A. Enumerated values
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
//case object PAWN2 extends Member
//B. from Enumeration [item M], required ordering of case objects
protected val orderedSet: List[Member] = List(KING, QUEEN, BISHOP, KNIGHT, ROOK, PAWN)
//required because object/class initialization ordering is not predictable
//if client happens to access ChessPiecesSimplest.ROOK first, the resulting object
// initialization order is:
//ROOK (to add), ChessPiecesSimplest, ROOK (after add), KING, QUEEN, BISHOP, KNIGHT PAWN
//C. from Enumeration [item B], required type extension
sealed trait Member extends MemberBase
//D. from Enumeration [item L]
override def typeTagMember: TypeTag[_] = typeTag[Member]
}
//
//E. Simple test for exhaustive pattern matching
// - just comment out any of the case entries
object ChessPiecesSimplestTestExhaustivePatternMatch {
def stringMe(chessPiece: ChessPiecesSimplest.Member): String =
chessPiece match {
case ChessPiecesSimplest.KING => "Of the World"
case ChessPiecesSimplest.QUEEN => "Of the World"
case ChessPiecesSimplest.BISHOP => "Of the Church"
case ChessPiecesSimplest.KNIGHT => "Of the Church"
case ChessPiecesSimplest.ROOK => "Of the Castle"
case ChessPiecesSimplest.PAWN => "Of the Field"
}
}
//Enumeration.scala
// - DIY Scala Enumeration (closest possible Java Enum equivalent with guaranteed pattern
// matching exhaustiveness checking)
//Copyright (C) 2014-2016 Jim O'Flaherty
//
//
//This Scala class is free software: you can redistribute it and/or modify it under the terms
//of the GNU General Public License as published by the Free Software Foundation, either
//version 3 of the License, or any later version.
//
//This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
//without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//See the GNU General Public License for more details.
//
//To see details of the GPLv3 License, please see <http://www.gnu.org/copyleft/gpl.html>.
//To see details of the GNU General Public License, please see <http://www.gnu.org/licenses/>.
//
//If you would like to obtain a custom/different/commercial license for this, please send an
//email with your request to <jim.oflaherty.jr@gmail.com>.
package org.public_domain.scala.utils
import scala.reflect.runtime.universe.{Symbol,TypeTag}
import scala.util.{Failure, Success, Try}
//import org.public_domain.scala.utils.Try_.{CompletedNoException,completedNoExceptionSingleton}
trait Enumeration {
//A. Placeholder Success type+singleton for Try
// - manually inlined from org.public_domain.scala.utils.Try_
sealed case class CompletedNoException()
final val completedNoExceptionSingleton = new CompletedNoException()
//B. Type Member which must be found and _sealed_ in the descendant object
// - see protected trait MemberBase [item O]
type Member <: MemberBase
//C. Collects all of the sealed class descendants of Member
// - depends upon protected (abstract) def typeTagMember [item L] being implemented in client descendant
// - making client implement is ugly
// - working to replace with experimental change to use getClass entry point for Member
private lazy val memberSealedTraitDescendantNames: Try[Set[String]] = {
def fetchMemberSealedTraitDescendantNames: Try[Set[String]] = {
def fetchSealedTraitDescendantsViaTraitTypeTag: Option[Set[Symbol]] = {
def fetchSealedTraitDescendantsViaTraitSymbol(symbol: Symbol): Option[Set[Symbol]] = {
val internal = symbol.asInstanceOf[scala.reflect.internal.Symbols#Symbol]
if (internal.isSealed)
Some(internal.sealedDescendants.map(_.asInstanceOf[Symbol]) - symbol)
else None
}
fetchSealedTraitDescendantsViaTraitSymbol(typeTagMember.tpe.typeSymbol)
}
fetchSealedTraitDescendantsViaTraitTypeTag match {
case Some(symbols) => Success(symbols.map(_.toString.stripPrefix("object ")))
case None => Failure(new IllegalStateException("reflection failed to obtain descendants of sealed trait Member"))
}
}
fetchMemberSealedTraitDescendantNames
}
//D. Adds a member to temporary internal storage
// - for errors, see val status in protected trait MemberBase [item O.D]
// - Does NOT generate the ordinal (due to JVM class initialization order - see val orderedSet below for all reasons)
@volatile
private var addFailure: Option[Throwable] = None
@volatile
private var isAddClosed: Boolean = false
@volatile
private var membersTempInternal: List[Member] = Nil
@volatile
private var memberNamesLowerCaseAsSetInternal: Set[String] = Set()
final private def add(member: Member): Try[CompletedNoException] = {
def postAddFailure(throwable: Throwable): Try[CompletedNoException] = {
val failure = Failure(throwable)
addFailure = Some(throwable)
failure
}
this.synchronized {
addFailure match {
case Some(throwable) => Failure(throwable)
case None =>
if (!isAddClosed)
memberSealedTraitDescendantNames.flatMap(memberSealedTraitDescendantNamesFm => {
val memberSealedTraitDescendantNamesFmLowerCase = memberSealedTraitDescendantNamesFm.map(_.toLowerCase)
if (getClass.getName.toLowerCase == member.nameSpace.toLowerCase)
if (!memberNamesLowerCaseAsSetInternal.contains(member.name.toLowerCase)) {
membersTempInternal = member :: membersTempInternal
memberNamesLowerCaseAsSetInternal = memberNamesLowerCaseAsSetInternal + member.name.toLowerCase
isAddClosed = memberNamesLowerCaseAsSetInternal == memberSealedTraitDescendantNamesFmLowerCase
if (isAddClosed)
if (orderedSet.nonEmpty) {
val orderedSetAsRealSet = orderedSet.toSet
if (orderedSet.size == orderedSetAsRealSet.size) {
val orderedSetNamesLowerCase = orderedSetAsRealSet.map(_.name.toLowerCase).toSet
if (orderedSetNamesLowerCase == memberSealedTraitDescendantNamesFmLowerCase)
//D.9 Info: Everything checks out and we're done;
// - all case object initializations equal the reflection discovered sealed trait
// descendant names equals the client provided orderedSet
Success(completedNoExceptionSingleton)
else {
//D.8 Paranoimia: Catch possible weird initialization effects
// - hard to imagine how this could occur
val inOnlyOrderedSet =
orderedSetNamesLowerCase.--(memberSealedTraitDescendantNamesFmLowerCase)
val inOnlyMemberSealedTraitDescendantNamesFm =
memberSealedTraitDescendantNamesFmLowerCase.--(orderedSetNamesLowerCase)
postAddFailure(
new IllegalStateException(
s"orderedSetNamesLowerCase [${orderedSetNamesLowerCase.mkString(",")}] is not equal to "
+ s"memberSealedTraitDescendantNamesFmLowerCase"
+ s" [${memberSealedTraitDescendantNamesFmLowerCase.mkString(",")}]"
+ (if (inOnlyOrderedSet.nonEmpty)
s" - orderedSetNamesLowerCase contains values [${inOnlyOrderedSet.mkString(",")}]"
+ s" not in memberSealedTraitDescendantNamesFmLowerCase"
else ""
)
+ (if (inOnlyMemberSealedTraitDescendantNamesFm.nonEmpty)
s" - memberSealedTraitDescendantNamesFmLowerCase contains values"
+ s" [${inOnlyMemberSealedTraitDescendantNamesFm.mkString(",")}] not in"
+ s" orderedSetNamesLowerCase"
else ""
)
)
)
}
}
else
//D.7 Guard: Client attempted to provide a non-unique List (there is no insertion order Set
// implementation in the Scala collections library)
postAddFailure(
new IllegalStateException(
s"orderedSet.size [${orderedSet.size}] must be equal to orderedSetAsRealSet.size"
+ s" [${orderedSetAsRealSet.size}] (isAddClosed is true)"
)
)
}
else
//D.6 Guard: Client attempted to provide an empty set
postAddFailure(new IllegalStateException("orderedSet must not be empty (isAddClosed is true)"))
else
if (memberNamesLowerCaseAsSetInternal.size != memberSealedTraitDescendantNamesFmLowerCase.size)
//D.5 Info: Initializations remain
Success(completedNoExceptionSingleton)
else {
//D.4 Paranoimia: Catch possible weird initialization effects
// - hard to imagine this could occur if the compiler is ensuring the sealed trait
// contract
val inOnlyMemberNamesLowerCaseAsSetInternal =
memberNamesLowerCaseAsSetInternal.--(memberSealedTraitDescendantNamesFmLowerCase)
val inOnlyMemberSealedTraitDescendantNamesFm =
memberSealedTraitDescendantNamesFmLowerCase.--(memberNamesLowerCaseAsSetInternal)
postAddFailure(
new IllegalStateException(
s"while being the same size [${memberNamesLowerCaseAsSetInternal.size}], both"
+ s" memberNamesLowerCaseAsSetInternal"
+ s" [${memberNamesLowerCaseAsSetInternal.mkString(",")}] and"
+ s" memberSealedTraitDescendantNamesFmLowerCase"
+ s" [${memberSealedTraitDescendantNamesFmLowerCase.mkString(",")}] are not equal (not sure how"
+ s" this is even possible)"
+ (if (inOnlyMemberNamesLowerCaseAsSetInternal.nonEmpty)
s" - memberNamesLowerCaseAsSetInternal contains values"
+ s" [${inOnlyMemberNamesLowerCaseAsSetInternal.mkString(",")}] not in"
+ s" memberSealedTraitDescendantNamesFmLowerCase"
else ""
)
+ (if (inOnlyMemberSealedTraitDescendantNamesFm.nonEmpty)
s" - memberSealedTraitDescendantNamesFmLowerCase contains values"
+ s" [${inOnlyMemberSealedTraitDescendantNamesFm.mkString(",")}] not in"
+ s" memberNamesLowerCaseAsSetInternal"
else ""
)
)
)
}
}
else
//D.3 Guard: Disallow case name sensitivity
// - KING and King are considered equivalent and whichever initializes second will be rejected
postAddFailure(
new IllegalArgumentException(
s"attempting to add member with name [${member.name}] which was previously "
+ s"(case insensitively) added [${member.name.toLowerCase}]"
)
)
else
//D.2 Watchful: Require all instances must come from the parent name space
postAddFailure(
new IllegalArgumentException(
s"member [${member.name}] (with nameSpace [${member.nameSpace}]) must be declared"
+ s"inside of the derived Enumeration's nameSpace [${getClass.getName}]"
)
)
}
)
else
//D.1 Paranoimia: Catch possible weird multi-threading conflicts
// - hard to imagine this could occur given how strongly defined JVM class
// initialization is defined
postAddFailure(
new IllegalStateException(s"attempting to add member [${member.name}] after isAddClosed was set true")
)
}
}
}
//E. Generates the ordinals for the members (only if all classes/objects initialized correctly)
// - uses lazy val to postpone defining/assigning ordinal until after all case object
// initializations complete i.e. just prior to completing client's first call
// - returns Try to facilitate external capturing/logging/reporting Failure (in def status below)
private lazy val membersInternal: Try[List[Member]] = {
this.synchronized {
addFailure match {
case Some(throwable) => Failure(throwable)
case None =>
if (isAddClosed)
Success(orderedSet)
else
if (orderedSet.nonEmpty) {
val orderedSetAsRealSet = orderedSet.toSet
if (orderedSet.size == orderedSetAsRealSet.size) {
val orderedSetNamesLowerCase = orderedSetAsRealSet.map(_.name.toLowerCase).toSet
memberSealedTraitDescendantNames.flatMap(memberSealedTraitDescendantNamesFm => {
val memberSealedTraitDescendantNamesFmLowerCase = memberSealedTraitDescendantNamesFm.map(_.toLowerCase)
val inOnlyOrderedSet = orderedSetNamesLowerCase.--(memberSealedTraitDescendantNamesFmLowerCase)
val inOnlyMemberSealedTraitDescendantNamesFm =
memberSealedTraitDescendantNamesFmLowerCase.--(orderedSetNamesLowerCase)
Failure(
new IllegalStateException(
s"orderedSetNamesLowerCase [${orderedSetNamesLowerCase.mkString(",")}] is not equal to "
+ s"memberSealedTraitDescendantNamesFmLowerCase"
+ s"[${memberSealedTraitDescendantNamesFmLowerCase.mkString(",")}]"
+ (if (inOnlyOrderedSet.nonEmpty)
s" - orderedSetNamesLowerCase contains values [${inOnlyOrderedSet.mkString(",")}] not in"
+ s" memberSealedTraitDescendantNamesFmLowerCase"
else ""
)
+ (if (inOnlyMemberSealedTraitDescendantNamesFm.nonEmpty)
s" - memberSealedTraitDescendantNamesFmLowerCase contains values"
+ s" [${inOnlyMemberSealedTraitDescendantNamesFm.mkString(",")}] not in orderedSetNamesLowerCase"
else ""
)
)
)
})
}
else
Failure(
new IllegalStateException(
s"orderedSet.size [${orderedSet.size}] must be equal to orderedSetAsRealSet.size"
+ s" [${orderedSetAsRealSet.size}] (isAddClosed is true)"
)
)
}
else
Failure(new IllegalStateException("orderedSet must not be empty"))
}
}
}
//F. Provide range covering all ordinals
final lazy val ordinalRange: Range =
ordinalRangeLift match {
case Success(range) => range
case Failure(exception) => throw exception
}
final lazy val ordinalRangeLift: Try[Range] =
if (ordinalStep != 0)
membersLift.flatMap(membersFm =>
Success(ordinalStart.until(ordinalStart + (membersFm.size * ordinalStep), ordinalStep))
)
else
Failure(new IllegalStateException(s"ordinalStep must not be equal to 0"))
//G. Enable lookup for ordinal by Member (used by Member.ordinal)
final lazy val ordinalByMember: Map[Member, Int] =
ordinalByMemberLift match {
case Success(map) => map
case Failure(exception) => throw exception
}
final lazy val ordinalByMemberLift: Try[Map[Member, Int]] =
membersInternal.flatMap(
membersInternalFm => {
ordinalRangeLift.flatMap(
ordinalRangeFm =>
Success(membersInternalFm.zip(ordinalRangeFm).toMap)
)
}
)
//H. Enable lookup for Member by ordinal
final lazy val memberByOrdinal: Map[Int, Member] =
memberByOrdinalLift match {
case Success(map) => map
case Failure(exception) => throw exception
}
final lazy val memberByOrdinalLift: Try[Map[Int, Member]] =
ordinalByMemberLift.flatMap(
ordinalByMemberFm => {
val ordinalAndMemberPairs =
ordinalByMemberFm.map(
memberAndOrdinalPair =>
(memberAndOrdinalPair._2, memberAndOrdinalPair._1)
)
Success(ordinalAndMemberPairs.toMap)
}
)
//I. Enable lookup for Member by name
private lazy val memberNames: String =
membersLift match {
case Success(membersLiftGet) => membersLiftGet.map(_.name).mkString(",")
case Failure(exception) => exception.getMessage
}
final def memberByName(name: String): Member =
memberByNameLift(name) match {
case Success(member) => member
case Failure(exception) => throw exception
}
final def memberByNameLift(name: String): Try[Member] =
memberByNameUpperCaseLift.flatMap(
map =>
map.get(name.toUpperCase) match {
case Some(member) =>
Success(member)
case None =>
Failure(new IllegalArgumentException(s"name [$name.toUpperCase] not found in member names [$memberNames]"))
}
)
final lazy val memberByNameUpperCase: Map[String, Member] =
memberByNameUpperCaseLift match {
case Success(map) => map
case Failure(exception) => throw exception
}
final lazy val memberByNameUpperCaseLift: Try[Map[String, Member]] =
membersInternal.flatMap(
membersInternalFm => {
val nameUpperCaseAndMemberPairs =
membersInternalFm.map(
member =>
(member.name.toUpperCase, member)
)
Success(nameUpperCaseAndMemberPairs.toMap)
}
)
//J. orderedSet of Members in their ordinal order
// - assuming stable class object initializations and validation, it is a reference to
// exactly the same data provided by the client in orderedSet)
final def members: List[Member] =
membersLift match {
case Success(list) => list
case Failure(exception) => throw exception
}
final def membersLift: Try[List[Member]] = membersInternal
//K. Provide Try status of initialization/validation
final lazy val status: Try[CompletedNoException] =
membersLift.flatMap(_ => Success(completedNoExceptionSingleton))
//L. Client provided reflection hook
// - ugly implementation requirement until I can figure out the route using Member.getClass
protected def typeTagMember: TypeTag[_]
//M. Required client provided ordering
// - orderedSet.size must be equal to orderedSet.toSet.size
// - client must declare as val to force immediate evaluation (or declare as lazy val and
// have another declared val depend upon on it similar to EnumerationDecorated's
// implementation)
// - required because object/class initialization ordering is not predictable
// - if client happens to access any explicit case object member (other than the first),
// the case object initialization order is undefined
// - required because the order of the returned classes from getClass.getDeclaredClasses
// is undefined
protected def orderedSet: List[Member]
//N. Optional client override for starting ordinal and step
protected def ordinalStart = 0
protected def ordinalStep = 1 //requirement: cannot equal 0
//O. Required client implementation to provide basis for unique Enumeration members
protected trait MemberBase {
self: Member =>
//O.A Overridden for serialization's readResolve to ensure singleton-ness
// by fetching existing instance via case insensitive name
private def readResolve(): Object =
memberByName(name)
//O.B Automatic naming based on getClass
// - all case objects must be in the same containing name space
// - name is case insensitive, IOW adding two different members where they only differ in
// letter casing is restricted; i.e. attempting to declare KING and King will cause
// an initialization exception
final def nameSpace: String =
getClass.getName.stripSuffix(getClass.getSimpleName)
final val name: String =
getClass.getSimpleName.stripSuffix("$")
//O.C The assigned ordinal based on Enumeration.order above
final lazy val ordinal: Int =
ordinalLift match {
case Success(int) => int
case Failure(exception) => throw exception
}
final lazy val ordinalLift: Try[Int] =
Enumeration.this.status match {
case Success(_) =>
ordinalByMemberLift match {
case Success(map) => Success(map(this))
case Failure(exception) => Failure(exception)
}
case Failure(exception) =>
Failure(
new IllegalStateException(s"access denied - Enumeration.status is a Failure - ${exception.getMessage}")
)
}
//O.D Provide Try status of specific Member initialization/validation
final val status: Try[CompletedNoException] = add(this)
}
}
//EnumerationDecorated.scala
// - DIY Scala Enumeration (closest possible Java Enum equivalent with guaranteed pattern
// matching exhaustiveness checking) with extended attributes within a decoration
//Copyright (C) 2014-2016 Jim O'Flaherty
//
//
//This Scala class is free software: you can redistribute it and/or modify it under the terms
//of the GNU General Public License as published by the Free Software Foundation, either
//version 3 of the License, or any later version.
//
//This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
//without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//See the GNU General Public License for more details.
//
//To see details of the GPLv3 License, please see <http://www.gnu.org/copyleft/gpl.html>.
//To see details of the GNU General Public License, please see <http://www.gnu.org/licenses/>.
//
//If you would like to obtain a custom/different/commercial license for this, please send an
//email with your request to <jim.oflaherty.jr@gmail.com>.
package org.public_domain.scala.utils
trait EnumerationDecorated extends Enumeration {
//A. Type Member which must be found in the descendant object
// - see protected trait DecorationBase [item B]
type Decoration <: DecorationBase
//B. From Enumeration [item M], required ordering of case objects
// it's pushed way down here to enable an iteration of decorationOrderedSet to avoid
// having to specify the members a third time
protected lazy val orderedSet: List[Member] = decorationOrderedSet.map(_.member)
//C. from Enumeration [item B], required type extension which must be overridden
// and _sealed_ by client
trait MemberDecorated extends MemberBase {
self: Member =>
def decoration: Decoration = decorationByMember(this)
}
//D. Defining the Decoration data
// - intended to be the only other time the Members must be mapped out right
// along with their specific decorative additions
val decorationOrderedSet: List[Decoration]
//E. Enable Decoration lookup by Member
lazy val decorationByMember: Map[Member, Decoration] =
decorationOrderedSet.map(decoration => (decoration.member, decoration)).toMap
//F. Required client implementation to provide basis for Decorations
trait DecorationBase {
self: Decoration =>
def member: Member
}
}
package org.public_domain.chess
object WorkSheetChessPiecesEnhancedDecoratedTest {
println("Welcome to the Scala worksheet")
//
import scala.util.{Failure, Success}
//
import org.public_domain.chess.{ChessPiecesEnhancedDecorated=>ChessPieces}
//
def stringMe(chessPiece: ChessPieces.Member): String =
chessPiece match {
case ChessPieces.KING => "Of the World"
case ChessPieces.QUEEN => "Of the World"
case ChessPieces.BISHOP => "Of the Church"
case ChessPieces.KNIGHT => "Of the Church"
case ChessPieces.ROOK => "Of the Castle"
case ChessPieces.PAWN => "Of the Field"
}
//to test various initialization ordering issues, rearrange these six lines into any order
val rook = ChessPieces.ROOK
val king = ChessPieces.KING
val pawn = ChessPieces.PAWN
val queen = ChessPieces.QUEEN
val bishop = ChessPieces.BISHOP
val knight = ChessPieces.KNIGHT
//
val statusChessPieces =
ChessPieces.status
val isStatusChessPiecesSuccess =
statusChessPieces.isSuccess
val statusChessPiecesMemberFailures =
ChessPieces.members.map(_.status).filter(_.isFailure)
val isStatusChessPiecesMembersSuccess =
statusChessPiecesMemberFailures.isEmpty
val isOrderedCorrectly =
ChessPieces.members == List(ChessPieces.KING, ChessPieces.QUEEN, ChessPieces.BISHOP, ChessPieces.KNIGHT, ChessPieces.ROOK, ChessPieces.PAWN)
val ordinalRange =
ChessPieces.ordinalRange
val ordinalNamePairs =
ChessPieces.members.map(member => (member.ordinal, member.name))
//
val bishopByName = ChessPieces.memberByName("bIsHop")
//
val decorationBishop = ChessPieces.decorationByMember(ChessPieces.BISHOP)
val decorations = ChessPieces.decorationOrderedSet
}
package org.public_domain.chess
object WorkSheetChessPiecesEnhancedTest {
println("Welcome to the Scala worksheet")
//
import scala.util.{Failure, Success}
//
import org.public_domain.chess.{ChessPiecesEnhanced=>ChessPieces}
//
def stringMe(chessPiece: ChessPieces.Member): String =
chessPiece match {
case ChessPieces.KING => "Of the World"
case ChessPieces.QUEEN => "Of the World"
case ChessPieces.BISHOP => "Of the Church"
case ChessPieces.KNIGHT => "Of the Church"
case ChessPieces.ROOK => "Of the Castle"
case ChessPieces.PAWN => "Of the Field"
}
//to test various initialization ordering issues, rearrange these six lines into any order
val rook = ChessPieces.ROOK
val king = ChessPieces.KING
val pawn = ChessPieces.PAWN
val queen = ChessPieces.QUEEN
val bishop = ChessPieces.BISHOP
val knight = ChessPieces.KNIGHT
//
val statusChessPieces =
ChessPieces.status
val isStatusChessPiecesSuccess =
statusChessPieces.isSuccess
val statusChessPiecesMemberFailures =
ChessPieces.members.map(_.status).filter(_.isFailure)
val isStatusChessPiecesMembersSuccess =
statusChessPiecesMemberFailures.isEmpty
val isOrderedCorrectly =
ChessPieces.members == List(ChessPieces.KING, ChessPieces.QUEEN, ChessPieces.BISHOP, ChessPieces.KNIGHT, ChessPieces.ROOK, ChessPieces.PAWN)
val ordinalRange =
ChessPieces.ordinalRange
val ordinalNamePairs =
ChessPieces.members.map(member => (member.ordinal, member.name))
//
val bishopByName = ChessPieces.memberByName("bIsHop")
//
val decorationBishop = ChessPieces.attributesByMember(ChessPieces.BISHOP)
val decorations = ChessPieces.attributesList
}
package org.public_domain.chess
object WorkSheetChessPiecesSimplestTest {
println("Welcome to the Scala worksheet")
//
import scala.util.{Failure, Success}
//
import org.public_domain.chess.{ChessPiecesSimplest=>ChessPieces}
//
def stringMe(chessPiece: ChessPieces.Member): String =
chessPiece match {
case ChessPieces.KING => "Of the World"
case ChessPieces.QUEEN => "Of the World"
case ChessPieces.BISHOP => "Of the Church"
case ChessPieces.KNIGHT => "Of the Church"
case ChessPieces.ROOK => "Of the Castle"
case ChessPieces.PAWN => "Of the Field"
}
//
//to test various initialization ordering issues, rearrange these six lines into any order
val rook = ChessPieces.ROOK
val king = ChessPieces.KING
val pawn = ChessPieces.PAWN
val queen = ChessPieces.QUEEN
val bishop = ChessPieces.BISHOP
val knight = ChessPieces.KNIGHT
//
val statusChessPieces =
ChessPieces.status
val isStatusChessPiecesSuccess =
statusChessPieces.isSuccess
val statusChessPiecesMemberFailures =
ChessPieces.members.map(_.status).filter(_.isFailure)
val isStatusChessPiecesMembersSuccess =
statusChessPiecesMemberFailures.isEmpty
val isOrderedCorrectly =
ChessPieces.members == List(ChessPieces.KING, ChessPieces.QUEEN, ChessPieces.BISHOP, ChessPieces.KNIGHT, ChessPieces.ROOK, ChessPieces.PAWN)
val ordinalRange =
ChessPieces.ordinalRange
val ordinalNamePairs =
ChessPieces.members.map(member => (member.ordinal, member.name))
//
val bishopByName = ChessPieces.memberByName("bIsHop")
}
@techmag
Copy link

techmag commented Feb 9, 2015

This is working like a charm - only had a little while to interact with it but the situations it "covers" seems to be very extensive. Looking forward to putting it through it's paces! It's author is very responsive.

@phani1kumar
Copy link

Hi, thank you for such an exhaustive and clear implementation for Enumeration in Scala. I am new to scala and for my project I am looking with similar requiremetns as you have outlined in Stackoverflow for Enumeration. On top of this I need the Enumeration object to be used inside my other member classes which I want to be able to pass through a REST API.

Meaning, I've an _EmployeeType as the enumeration with values {"contractor", "permanent"} and I've an employee class with {"id", "name", "type: _EmployeeType} as the member class.

Hence to be able to expose the Employee REST API, I should be able to stream the Employee object through jsonFormat3(Employee). But now I am not able to convert the _EmployeeType into a jsonFormat protocol. Can you please help?

PS: I am using spray-json library for the JSON to object and vice versa conversion.
Thanks and Regards,
Phani

@chaotic3quilibrium
Copy link
Author

@phani1kumar

Hi! I've just now (2015/Oct/02) saw your message. It's much better to hit me at my personal email jim dot oflaherty dot jr at gmail dot com as I see that every day.

I am not currently using that library. So I am not familiar enough with what you might be hitting. What kind of error are you receiving?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment