1. Introduction
An enumeration refers to a group of named constants. **Scala 2 provides an abstract class called Enumeration to create and retrieve enumerations. Scala 3 provides a better way to create enumerations using the keyword enum.
**
In this tutorial, we’ll see how to extend the Enumeration class and customize it further. We’ll also see how to use the improved Scala 3 enumerations.
2. Enumerations in Scala 2
Let’s take a simple example of enumerating the fingers of a hand.
2.1. Creating an Enumeration
We’ll begin by creating a new enumeration to represent the fingers:
object Fingers extends Enumeration {
type Finger = Value
val Thumb, Index, Middle, Ring, Little = Value
}
The Enumeration class provides a type called Value to represent each of the enumeration values. It also provides a nifty protected method by the same name to create and initialize the values.
It’s advisable to not add values to an enumeration after its construction, as it’s not thread-safe.
2.2. Retrieving the Values
Let’s write a simple function to check if a finger is the shortest:
class FingersOperation {
def isShortest(finger: Finger) = finger == Little
}
and verify the same in a unit test:
@Test
def givenAFinger_whenIsShortestCalled_thenCorrectValueReturned() = {
val operation = new FingersOperation()
assertTrue(operation.isShortest(Little))
assertFalse(operation.isShortest(Index))
}
Let’s now try writing a function to return the two longest fingers.
To do this, we need a way to iterate over the enumeration values. The Values method in the Enumeration class comes to the rescue:
def twoLongest() =
Fingers.values.toList.filter(finger => finger == Middle || finger == Index)
The Values method returns an ordered set of enumeration values.
Let’s test the twoLongest function as well:
@Test
def givenFingers_whenTwoLongestCalled_thenCorrectValuesReturned() = {
val operation = new FingersOperation()
assertEquals(List(Index, Middle), operation.twoLongest())
}
2.3. Overriding Identifier and Name
Each enumeration value has an identifier and a name. By default, each value is assigned an identifier starting from 0 and a name that is the same as the value itself.
We can validate the defaults in a simple test:
@Test
def givenAFinger_whenIdAndtoStringCalled_thenCorrectValueReturned() = {
assertEquals(0, Thumb.id)
assertEquals("Little", Little.toString())
}
The Value method in the Enumeration class allows us to change the defaults:
val Thumb = Value(1, "Thumb Finger")
val Index = Value(2, "Pointing Finger")
val Middle = Value(3, "The Middle Finger")
val Ring = Value(4, "Finger With The Ring")
val Little = Value(5, "Shorty Finger")
Let’s modify our test to verify the changes:
assertEquals(1, Thumb.id)
assertEquals("Shorty Finger", Little.toString())
2.4. Deserializing an Enumeration Value
If we want to get the enumeration value from a valid name, the withName method comes in handy:
assertEquals(Middle, Fingers.withName("The Middle Finger"))
However, if we try to deserialize a non-existent value, we’ll get a java.util.NoSuchElementException.
2.5. Changing the Ordering
The Values method in the Enumeration class returns a set of values, sorted by their identifiers. Let’s check the default ordering of our fingers enumeration:
assertEquals(List(Thumb, Index, Middle, Ring, Little), Fingers.values.toList)
Now let’s change the ordering so that the Thumb comes at the end of the enumeration. All we need to do is to make the identifier of the Thumb the largest:
val Thumb = Value(6, "Thumb Finger")
// Other enumeration values defined previously
Let’s assert the new ordering:
assertEquals(List(Index, Middle, Ring, Little, Thumb), Fingers.values.toList)
3. Adding Attributes to an Enumeration
Our isShortest and twoLongest functions use hard-coded values of the enumeration for comparison. Instead, it’d be better if the height of a finger is used. So, let’s try to encapsulate this new attribute in our Fingers enumeration*.*
The Enumeration class provides an inner class named Val, which can be extended to add additional attributes:
protected case class FingerDetails(i: Int, name: String, height: Double)
extends super.Val(i, name) {
def heightInCms(): Double = height * 2.54
}
In order to use height and heightInCms, we also need to provide an implicit type conversion for the FingerDetails class:
import scala.language.implicitConversions
implicit def valueToFingerDetails(x: Value): FingerDetails =
x.asInstanceOf[FingerDetails]
Now we’re all set to populate the height of each of the fingers:
val Thumb = FingerDetails(6, "Thumb Finger", 1)
val Index = FingerDetails(2, "Pointing Finger", 4)
val Middle = FingerDetails(3, "The Middle Finger", 4.1)
val Ring = FingerDetails(4, "Finger With The Ring", 3.2)
val Little = FingerDetails(5, "Shorty Finger", 0.5)
Finally, let’s modify the isShortest and twoLongest functions to use the new attributes and calculate the result:
def isShortest(finger: Finger) =
Fingers.values.toList.sortBy(_.height).head == finger
def twoLongest() =
Fingers.values.toList.sortBy(_.heightInCms()).takeRight(2)
4. Problems and Alternatives
4.1. Problems with Enumeration
There are a couple of major issues with using the Enumeration class.
First, all enumerations have the same type after erasure. So, we can’t have overloaded methods, even with different enumerations as arguments:
object Operation extends Enumeration {
type Operation = Value
val Plus, Minus = Value
}
object Conflicts {
// compile error
def getValue(f: Fingers.Finger) = f.toString
def getValue(o: Operation.Operation) = o.toString
}
Second, the Scala compiler does not do an exhaustiveness check for case matches. For example, there will be no compilation error for the below function:
def checkIfIndex(finger: Finger) = {
finger match {
case Index => true
}
}
The above function also works fine as long as we pass in Index as the argument:
val operation = new FingersOperation()
assertTrue(operation.checkIfIndex(Index))
However, if we send in any other argument that is not covered by the case match, we’ll get a scala.MatchError.
4.2. Comparison with Sealed Traits
A better alternative to creating enumerations is to use Sealed Traits (along with case objects), as they provide compile-time safety for case matches. However, they come with their own baggage of problems:
- Sealed traits do not provide an out-of-the-box solution to list all the enumeration values
- There is no easy way to deserialize a case object from the enumeration name
- Case objects do not have a default order based on identifiers – we need to manually include the identifier as an attribute in the sealed trait and provide an ordering function
5. Enumerations in Scala 3
Scala 3 brings a significant overhaul to the language, including a rewrite of the compiler. Alongside this rewrite, various syntax enhancements and feature refinements are introduced, notably impacting how enumerations are handled.
In Scala 3, a new keyword, enum, is introduced to define enumerations. In this section, let’s look at some of the features of enum in Scala 3n.
5.1. Creating Enumerations
Let’s use the same scenario as we used in Scala 2 section. We can create enumerations for fingers:
enum Fingers:
case Thumb, Index, Middle, Ring, Little
This code above defines the enumeration values for fingers using the enum and case keywords. The individual values can be accessed as Fingers.Thumb, Fingers.Index and so on. Internally, the Scala compiler uses a combination of sealed traits and case objects to define each value, however, this is abstracted away from the developers.
5.2. Adding Attributes to enum
Scala 3 simplifies the process of defining enumerated types with attached attributes:
enum Fingers(val height: Double):
case Thumb extends Fingers(1)
case Index extends Fingers(4)
case Middle extends Fingers(4.1)
case Ring extends Fingers(3.2)
case Little extends Fingers(0.5)
We can access the height value directly on the enum instance:
Fingers.Thumb.height //returns 1.0
With attributes, enums become more versatile, allowing for better data representation within the enumeration.
5.3. Defining Custom Methods
Just like classes, enumerations in Scala 3 can have custom methods:
enum Fingers(val height: Double):
case Thumb extends Fingers(1)
case Index extends Fingers(4)
case Middle extends Fingers(4.1)
case Ring extends Fingers(3.2)
case Little extends Fingers(0.5)
def heightInCms(): Double = height * 2.54
We can get the height in centimeters for each finger using the defined method:
Fingers.Thumb.heightInCms() //returns 2.54
With this example, we convert the height of the Thumb into centimeters using a custom method, showcasing the flexibility and expressiveness of Scala 3’s enumeration feature.
6. Conclusion
In this article, we saw how to create, retrieve, and customize enumerations in Scala 2 and Scala 3. Furthermore, the introduction of the enum keyword in Scala 3 addressed the limitations faced by enumerations in Scala 2.
As always, the full source code for the examples is available over on GitHub.