Case objects vs Enumerations in Scala

前端 未结 14 1008
误落风尘
误落风尘 2020-11-22 16:45

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.

相关标签:
14条回答
  • 2020-11-22 17:26

    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() = ...
    }
    
    0 讨论(0)
  • 2020-11-22 17:28

    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.

    0 讨论(0)
  • 2020-11-22 17:30

    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 annotation

    The 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 on Scala.js/Scala-Native, Scala source code not able to define enums that are accepted by existing APIs on Scala-JVM).

    0 讨论(0)
  • 2020-11-22 17:31

    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 }
    }
    
    0 讨论(0)
  • 2020-11-22 17:32

    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
    }
    
    0 讨论(0)
  • 2020-11-22 17:34

    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!

    0 讨论(0)
提交回复
热议问题