Search code examples
scalaenums

Using Enumerations in Scala Best Practices


I have been using sealed traits and case objects to define enumerated types in Scala and I recently came across another approach to extend the Enumeration class in Scala like this below:

object CertificateStatusEnum extends Enumeration {
  val Accepted, SignatureError, CertificateExpired, CertificateRevoked, NoCertificateAvailable, CertChainError, ContractCancelled = Value
}

against doing something like this:

sealed trait CertificateStatus
object CertificateStatus extends {
  case object Accepted               extends CertificateStatus
  case object SignatureError         extends CertificateStatus
  case object CertificateExpired     extends CertificateStatus
  case object CertificateRevoked     extends CertificateStatus
  case object NoCertificateAvailable extends CertificateStatus
  case object CertChainError         extends CertificateStatus
  case object ContractCancelled      extends CertificateStatus
}

What is considered a good approach?


Solution

  • They both get the job done for simple purposes, but in terms of best practice, the use of sealed traits + case objects is more flexible.

    The story behind is that since Scala came with everything Java had, so Java had enumerations and Scala had to put them there for interoperability reasons. But Scala does not need them, because it supports ADTs (algebraic data types) so it can generate enumeration in a functional way like the one you just saw.

    You'll encounter certain limitations with the normal Enumeration class:

    • the inability of the compiler to detect pattern matches exhaustively
    • it's actually harder to extend the elements to hold more data besides the String name and the Int id, because Value is final.
    • at runtime, all enums have the same type because of type erasure, so limited type level programming - for example, you can't have overloaded methods.
    • when you did object CertificateStatusEnum extends Enumeration your enumerations will not be defined as CertificateStatusEnum type, but as CertificateStatusEnum.Value - so you have to use some type aliases to fix that. The problem with this is the type of your companion will still be CertificateStatusEnum.Value.type so you'll end up doing multiple aliases to fix that, and have a rather confusing enumeration.

    On the other hand, the algebraic data type comes as a type-safe alternative where you specify the shape of each element and to encode the enumeration you just need sum types which are expressed exactly using sealed traits (or abstract classes) and case objects.

    These solve the limitations of the Enumeration class, but you'll encounter some other (minor) drawbacks, though these are not that limiting:

    • case objects won't have a default order - so if you need one, you'll have to add your id as an attribute in the sealed trait and provide an ordering method.
    • a somewhat problematic issue is that even though case objects are serializable, if you need to deserialize your enumeration, there is no easy way to deserialize a case object from its enumeration name. You will most probably need to write a custom deserializer.
    • you can't iterate over them by default as you could using Enumeration. But it's not a very common use case. Nevertheless, it can be easily achieved, e.g. :
      object CertificateStatus extends {
        val values: Seq[CertificateStatus] = Seq(
          Accepted,
          SignatureError,
          CertificateExpired,
          CertificateRevoked,
          NoCertificateAvailable,
          CertChainError,
          ContractCancelled
        )
        // rest of the code
      }
    

    In practice, there's nothing that you can do with Enumeration that you can't do with sealed trait + case objects. So the former went out of people's preferences, in favor of the latter. This comparison only concerns Scala 2.

    In Scala 3, they unified ADTs and their generalized versions (GADTs) with enums under a new powerful syntax, effectively giving you everything you need. So you'll have every reason to use them. As Gael mentioned, they became first-class entities.