Enumeration is a finite set of ordered values representing a domain or a collection. Enumerations or Enums are very widely used in all the programming languages to model the domain. Like other languages, Scala also supports enumerations. However, there are many practical problems with Scala's in-built enum type. However, there is Enumeratum, a better enumeration library for Scala.
There are mainly two ways in which we can create enumerations in Scala.
The in-built method to create enumerations in Scala is by extending with the abstract class Enumeration. However, there are some major issues with this approach:
- Enumerations are erased at run-time.
- No exhaustive check for pattern matching at compile time
Due to the issues with Scala's Enumeration, enumerations are created as ADTs using sealed traits and case objects. This solved some of the issues. However, there are still some drawbacks with this approach:
- No out-of-the-box solution to build and list enumerations
- No automatic ordering for the items
- Need to write custom logic for serialization and deserialization
Enumeratum library tries to solve all the previously mentioned issues. Some of the advantages of using Enumeratum are:
- Exhaustive pattern-matching warning
- Very fast and easy to use
- Many in-built utility methods to customize enumerations
- Very good integration with popular JSON and Database libraries
To use Enumeratum, we need to add the library-dependency:
libraryDependencies ++= Seq(
"com.beachape" %% "enumeratum" % "1.7.0"
)
Now we can use the below import statement and the required classes will be available in scope:
import enumeratum._
Let's look at how we can define a simple enum. We will be using the types EnumEntry and Enum to define our enumerations.
Firstly, we need to create a sealed trait or abstract class to define our enum type. For this, we need to extend the trait EnumEntry:
sealed trait Country extends EnumEntry
Now we can define the supported list of countries:
object Country extends Enum[Country] {
case object Germany extends Country
case object India extends Country
override val values: IndexedSeq[Country] = findValues
}
Note that we have to implement the field values, with the list of possible values for the enum. Enumeratum already provides a method findValues which returns all the defined enum elements in the same order in which it is defined. This is done at compile time using macros and hence there is no run-time overhead too.
We can verify the above implementation:
val countries = Country.values
assert(countries.size == 2)
assert(countries.equals(Seq(Country.Germany, Country.India)))
Enumeratum provides many utility methods on enum values. Let's look at some of the popular and useful ones.
To convert a String value to enum, we can use withName method:
Country.withName("India")
If the value is not a valid enum name, it will throw a NoSuchElementException at runtime. But to handle such cases, we can use withNameOption method:
val usa: Option[Country] = Country.withNameOption("USA")
It will return None if the value passed is an invalid enum name, otherwise, it will be wrapped in Some.
We can also handle case sensitiveness using different available utility methods:
val germanyCaps = Country.withNameInsensitive("GERMANY")
assert(germanyCaps == Country.Germany)
val indiaMixed = Country.withNameInsensitive("iNDia")
assert(indiaMixed == Country.India)
val usaInsensitive = Country.withNameInsensitiveOption("uSA")
assert(usaInsensitive.isEmpty)
val germanyIndex = Country.indexOf(Country.Germany)
assert(germanyIndex == 0)
By default, the enum value will be the same as the case object name. However, we can customize this name by overriding the entryName parameter:
sealed abstract class Gender(override val entryName: String) extends EnumEntry
object Gender extends Enum[Gender]{
override def values: IndexedSeq[Gender] = findValues
case object Male extends Gender("M")
case object Female extends Gender("F")
case object Other extends Gender("O")
}
Now, the enum values will be overwritten to M, F, O instead of Male, Female, and Other:
val male = Gender.withName("M")
assert(male == Gender.Male)
Instead of overriding the enum values explicitly, Enumeratum provides some utility traits for common scenarios. We can use these traits to extend our enum entries based on the requirement. These traits will internally override the entryName parameter with some of the common String conversions. Let's look at an example:
sealed trait NamingConvention extends EnumEntry with EnumEntry.LowerCamelcase
object NamingConvention extends Enum[NamingConvention] {
override val values: IndexedSeq[NamingConvention] = findValues
case object JavaStyle extends NamingConvention
case object ScalaStyle extends NamingConvention
case object PythonStyle extends NamingConvention with EnumEntry.Snakecase
}
The trait NamingConvention is extending a trait EnumEntry.LowerCamelcase. The trait will modify the entryName parameter. This is equivalent to explicitly defining JavaStyle enum as:
sealed abstract class NamingConvention(override val entryName: String) extends EnumEntry
object NamingConvention extends Enum[NamingConvention] {
override val values: IndexedSeq[NamingConvention] = findValues
case object JavaStyle extends NamingConvention("javaStyle")
case object ScalaStyle extends NamingConvention("scalaStyle")
case object PythonStyle extends NamingConvention("python_style")
}
Note that javaStyle and scalaStyle are using camel case format, whereas python_style is using snake case format. Apart from Snakecase and LowerCamelcase traits, some of the other implementations are Uppercase, Lowercase, CapitalHyphencase, and so on.
Apart from the regular String-based enums, Enumeratum also supports enums for types like Int, Long, Char, and so on. Instead of extending with EnumEntry, we need to use the more specific type like IntEnumEntry, LongEnumEntry, etc:
import enumeratum.values._
sealed abstract class HttpCode(val value:Int, val name:String) extends IntEnumEntry
object HttpCode extends IntEnum[HttpCode] {
override val values: IndexedSeq[HttpCode] = findValues
case object OK extends HttpCode(200, "Ok")
case object BadRequest extends HttpCode(400, "Bad Request")
}
Now, we can create the enum using the integer value:
val bad = HttpCode.withValue(400)
assert(bad == HttpCode.BadRequest)
assert(bad.name == "Bad Request")
Enumeratum has very good integration with other libraries. It integrates well with JSON libraries as well as database libraries. Some of the supported libraries are Json4s, Play, Slick, ReactiveMongo, Circe, and many others. The full list of integrations is available here.
Prior to Scala-3, the enumeration in the standard library is far from perfect. However, in Scala-3, the enumerations are completely redesigned and all the major issues in the previous versions are solved. But, if it is not possible to upgrade to Scala-3, then Enumeratum is the best solution for managing enums.