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?
CodePudding user response:
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 theInt
id, becauseValue
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 asCertificateStatusEnum
type, but asCertificateStatusEnum.Value
- so you have to use some type aliases to fix that. The problem with this is the type of your companion will still beCertificateStatusEnum.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 interfaces
(or abstract classes
) and case objects
.
These solve the issues above, but you will encounter 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 thesealed 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 by adding some code to it, e.g. :
object CertificateStatus extends {
val values: Seq[CertificateStatus] = Seq(
Accepted,
SignatureError,
CertificateExpired,
CertificateRevoked,
NoCertificateAvailable,
CertChainError,
ContractCancelled
)
// rest of the code
}
In reality, there is 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 have every reason to use them. As Gael mentioned, they become first-class entities.
CodePudding user response:
It depends on what you want from enum.
In the first case, you implicitly have an order on items (accessed by id property). Reordering has consequences.
I'd prefer 'case object', in some cases enum item could have extra info in the constructor (like, Color with RGB, not just name).
Also, I'd recommend https://index.scala-lang.org/mrvisser/sealerate or similar libraries. That allows iterating over all elements.