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.
I think the biggest advantage of having case classes
over enumerations
is that you can use type class pattern a.k.a ad-hoc polymorphysm. Don't need to match enums like:
someEnum match {
ENUMA => makeThis()
ENUMB => makeThat()
}
instead you'll have something like:
def someCode[SomeCaseClass](implicit val maker: Maker[SomeCaseClass]){
maker.make()
}
implicit val makerA = new Maker[CaseClassA]{
def make() = ...
}
implicit val makerB = new Maker[CaseClassB]{
def make() = ...
}
I've been going back and forth on these two options the last few times I've needed them. Up until recently, my preference has been for the sealed trait/case object option.
1) Scala Enumeration Declaration
object OutboundMarketMakerEntryPointType extends Enumeration {
type OutboundMarketMakerEntryPointType = Value
val Alpha, Beta = Value
}
2) Sealed Traits + Case Objects
sealed trait OutboundMarketMakerEntryPointType
case object AlphaEntryPoint extends OutboundMarketMakerEntryPointType
case object BetaEntryPoint extends OutboundMarketMakerEntryPointType
While neither of these really meet all of what a java enumeration gives you, below are the pros and cons:
Scala Enumeration
Pros: -Functions for instantiating with option or directly assuming accurate (easier when loading from a persistent store) -Iteration over all possible values is supported
Cons: -Compilation warning for non-exhaustive search is not supported (makes pattern matching less ideal)
Case Objects/Sealed traits
Pros: -Using sealed traits, we can pre-instantiate some values while others can be injected at creation time -full support for pattern matching (apply/unapply methods defined)
Cons: -Instantiating from a persistent store - you often have to use pattern matching here or define your own list of all possible 'enum values'
What ultimately made me change my opinion was something like the following snippet:
object DbInstrumentQueries {
def instrumentExtractor(tableAlias: String = "s")(rs: ResultSet): Instrument = {
val symbol = rs.getString(tableAlias + ".name")
val quoteCurrency = rs.getString(tableAlias + ".quote_currency")
val fixRepresentation = rs.getString(tableAlias + ".fix_representation")
val pointsValue = rs.getInt(tableAlias + ".points_value")
val instrumentType = InstrumentType.fromString(rs.getString(tableAlias +".instrument_type"))
val productType = ProductType.fromString(rs.getString(tableAlias + ".product_type"))
Instrument(symbol, fixRepresentation, quoteCurrency, pointsValue, instrumentType, productType)
}
}
object InstrumentType {
def fromString(instrumentType: String): InstrumentType = Seq(CurrencyPair, Metal, CFD)
.find(_.toString == instrumentType).get
}
object ProductType {
def fromString(productType: String): ProductType = Seq(Commodity, Currency, Index)
.find(_.toString == productType).get
}
The .get
calls were hideous - using enumeration instead I can simply call the withName method on the enumeration as follows:
object DbInstrumentQueries {
def instrumentExtractor(tableAlias: String = "s")(rs: ResultSet): Instrument = {
val symbol = rs.getString(tableAlias + ".name")
val quoteCurrency = rs.getString(tableAlias + ".quote_currency")
val fixRepresentation = rs.getString(tableAlias + ".fix_representation")
val pointsValue = rs.getInt(tableAlias + ".points_value")
val instrumentType = InstrumentType.withNameString(rs.getString(tableAlias + ".instrument_type"))
val productType = ProductType.withName(rs.getString(tableAlias + ".product_type"))
Instrument(symbol, fixRepresentation, quoteCurrency, pointsValue, instrumentType, productType)
}
}
So I think my preference going forward is to use Enumerations when the values are intended to be accessed from a repository and case objects/sealed traits otherwise.
Update March 2017: as commented by Anthony Accioly, the scala.Enumeration/enum
PR has been closed.
Dotty (next generation compiler for Scala) will take the lead, though dotty issue 1970 and Martin Odersky's PR 1958.
Note: there is now (August 2016, 6+ years later) a proposal to remove scala.Enumeration
: PR 5352
Deprecate
scala.Enumeration
, add@enum
annotationThe syntax
@enum
class Toggle {
ON
OFF
}
is a possible implementation example, intention is to also support ADTs that conform to certain restrictions (no nesting, recursion or varying constructor parameters), e. g.:
@enum
sealed trait Toggle
case object ON extends Toggle
case object OFF extends Toggle
Deprecates the unmitigated disaster that is
scala.Enumeration
.Advantages of @enum over scala.Enumeration:
- Actually works
- Java interop
- No erasure issues
- No confusing mini-DSL to learn when defining enumerations
Disadvantages: None.
This addresses the issue of not being able to have one codebase that supports Scala-JVM,
Scala.js
and Scala-Native (Java source code not supported onScala.js/Scala-Native
, Scala source code not able to define enums that are accepted by existing APIs on Scala-JVM).
I prefer case objects
(it's a matter of personal preference). To cope with the problems inherent to that approach (parse string and iterate over all elements), I've added a few lines that are not perfect, but are effective.
I'm pasting you the code here expecting it could be useful, and also that others could improve it.
/**
* Enum for Genre. It contains the type, objects, elements set and parse method.
*
* This approach supports:
*
* - Pattern matching
* - Parse from name
* - Get all elements
*/
object Genre {
sealed trait Genre
case object MALE extends Genre
case object FEMALE extends Genre
val elements = Set (MALE, FEMALE) // You have to take care this set matches all objects
def apply (code: String) =
if (MALE.toString == code) MALE
else if (FEMALE.toString == code) FEMALE
else throw new IllegalArgumentException
}
/**
* Enum usage (and tests).
*/
object GenreTest extends App {
import Genre._
val m1 = MALE
val m2 = Genre ("MALE")
assert (m1 == m2)
assert (m1.toString == "MALE")
val f1 = FEMALE
val f2 = Genre ("FEMALE")
assert (f1 == f2)
assert (f1.toString == "FEMALE")
try {
Genre (null)
assert (false)
}
catch {
case e: IllegalArgumentException => assert (true)
}
try {
Genre ("male")
assert (false)
}
catch {
case e: IllegalArgumentException => assert (true)
}
Genre.elements.foreach { println }
}
For those still looking how to get GatesDa's answer to work: You can just reference the case object after declaring it to instantiate it:
trait Enum[A] {
trait Value { self: A =>
_values :+= this
}
private var _values = List.empty[A]
def values = _values
}
sealed trait Currency extends Currency.Value
object Currency extends Enum[Currency] {
case object EUR extends Currency;
EUR //THIS IS ONLY CHANGE
case object GBP extends Currency; GBP //Inline looks better
}
If you are serious about maintaining interoperability with other JVM languages (e.g. Java) then the best option is to write Java enums. Those work transparently from both Scala and Java code, which is more than can be said for scala.Enumeration
or case objects. Let's not have a new enumerations library for every new hobby project on GitHub, if it can be avoided!