1. Introduction

An enumeration refers to a group of named constants. Scala provides an abstract class called Enumeration to create and retrieve enumerations. 

In this tutorial, let’s see how to extend the Enumeration class and customize it further.

2. Example

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. Conclusion

In this article, we saw how to create, retrieve, and customize enumerations.

As always, the full source code for the examples is available over on GitHub.

guest
0 Comments
Inline Feedbacks
View all comments