Enums in Scala

on waitingforcode.com

Enums in Scala

Some people coming from Java are often confused about enumerations. In Java, they're often used not only to enumerate things as in dictionaries but also to create singletons. In Scala, the latter use case is much more reserved to objects and apparently, only the former one remains. We'll see through this post whether it's true or not.

The post presents the use of enums in Scala. The first section shows some basic features while the second focuses on the enumerations and the pattern matching.

Using Scala enums

Enumerations in Scala aren't much different from the enumerations in other languages. They're also used to model a finite set of entities. In Scala we define them by extending Enumeration class:

describe("enumeration-based enum") {
  it("should be created by extending Enumeration") {
    object Languages extends Enumeration {
      type Languages = Value
      val Scala, Java, C = Value
    }

    Languages shouldBe a [Enumeration]
    Languages.values should have size 3
    Languages.values should contain allOf(Languages.Scala, Languages.Java, Languages.C)
  }

The definition order does matter since it determines the order of returned entities from values method:

it("should have the entries sorted by declaration order") {
  object Months extends Enumeration {
    type Months = Value
    val March, April, January, February = Value
  }

  Months.values.toSeq should contain inOrderElementsOf (Seq(
    Months.March, Months.April, Months.January
  ))
}

But the order can be changed with the use of sorted operation:

it("should reorder the entries") {
  object Priorities extends Enumeration {
    type Priorities = Value
    val High = Value(3)
    val Low = Value(1)
    val Medium = Value(2)
  }

  val sortedPriorities = Priorities.values.toList.sorted

  sortedPriorities should contain inOrderElementsOf(Seq(
    Priorities.Low, Priorities.Medium, Priorities.High
  ))
}

To apply custom order, Scala uses def compare(that: Value): Int of an object called Value. As you could see in previous examples, the instances of this object are created to represent the entities stored in the enumeration. By default, they're composed of an integer number representing their internal id used for ordering. However we can override this automatically assigned value and in addition to that, we can specify a name:

it("should create enumeration with a value") {
  object Letters extends Enumeration {
    type Letters = Value
    val A = Value(4, "A")
    val B = Value(3, "B")
    val C = Value(2, "C")
    val D = Value(1, "D")
  }

  Letters.B.id shouldEqual 3
  Letters.B.toString shouldEqual "B"
}

Since the enumerations are also the objects we can add methods or other properties to them. In the next example we override withName(name: String) method that behaves similarly to Java's valueOf(String name) and retrieves or not the enumeration entity corresponding the value passed as the argument. However it's unsafe because when the entity doesn't exist, an exception is thrown. To overcome the issue we can define our own exception-free method:

it("should fail when the entry doesn't exist") {
  object Letters extends Enumeration {
    type Letters = Value
    val A = Value
  }

  val error = intercept[NoSuchElementException] {
    Letters.withName("B")
  }

  error.getMessage shouldEqual "No value found for 'B'"
}

it("should create enumeration with method") {
  object Letters extends Enumeration {
    type Letters = Value

    val A, B, C = Value

    def safelyGetLetterOrNone(letterToGet: String): Option[Letters] = {
      try {
        Some(withName(letterToGet))
      } catch {
        case NonFatal(_) => None
      }
    }
  }

  Letters.safelyGetLetterOrNone("D") shouldBe None
}

Above use case is only one of the problems the community accuses Scala's enumerations. Another one is the lack of compilation warning in the case of non-exhaustive match:

it("should compile without warnings but fail at runtime") {
  object Letters extends Enumeration {
    type Letters = Value
    val A, B, C, D = Value
  }

  // Usually such construction warns about non exhaustiveness of the pattern
  // matching. But it's not true for enumerations and the code compiles without
  // warnings.
  val matchError = intercept[MatchError] {
    Letters.D match {
      case Letters.A => "A"
      case Letters.B => "B"
      case Letters.C => "C"
    }
  }

  matchError.getMessage() shouldEqual "D (of class scala.Enumeration$Val)"
}

Because of that, and other limitations as the possibility to define only 2 fields: a numerical id and a textual name, some alternative approaches to deal with enumerations were proposed.

Enums and pattern matching

One very common advice to substitute Enumeration-based enums is to use sealed trait and case objects. With them, the compiler is able to warn us about the non-exhaustive pattern matching check. Also, such enumerations can have more fields than id and name. However, they're not the proper enumerations. They aren't ordered and often they're not grouped together that complicates their retrieval. But if we accept the last drawback, they become pretty legitimate solution:

describe("sealed class-based enum") {
  it("should address most of Enumeration-based issues") {
    sealed trait Letter {
      def upperCase: String
      def lowerCase: String

      override def toString: String = s"${upperCase}_${lowerCase}"
    }
    object Letters {
      case object A extends Letter {
        override def upperCase: String = "A"
        override def lowerCase: String = "a"
      }
      case object B extends Letter {
        override def upperCase: String = "B"
        override def lowerCase: String = "b"
      }
      case object C extends Letter {
        override def upperCase: String = "C"
        override def lowerCase: String = "c"
      }
      case object D extends Letter {
        override def upperCase: String = "D"
        override def lowerCase: String = "d"
      }

      val values = Seq(D, C, B, A)
    }
    
    // Unlike Enumeration-based enum, here the pattern matching is checked at compilation time
    // Warning:(31, 5) match may not be exhaustive.
    // It would fail on the following inputs: A, B, C
    //    letter match {
    def matchLetter(letter: Letter): Unit = {
      letter match {
        case Letters.D => "letter is a D"
      }
    }

    matchLetter(Letters.D) shouldEqual "letter is a D"
    Letters.A.upperCase shouldEqual "A"
    Letters.A shouldNot equal(Letters.B)
    Letters.values.sortBy(_.upperCase) should contain inOrderElementsOf(Seq(
      Letters.A, Letters.B, Letters.C, Letters.D
    ))
  }
}

Please notice that there are also some Open Source solutions enhancing enumerations in Scala. Apparently, the enumerations from the next Scala version called Dotty will look very close to the ones in Java and we'll be able to, for instance, parameterize them.

This short post presented the basic points about Scala enumerations. As we could see, they're not so evolved than in Java. They can accept only 2 values (id and name) and seem to be considered only as pure dictionaries. Fortunately, they can be declared in different, more extendible manner, through sealed trait and its implementations. But on the other side, this custom construct may be misknown for the people just coming to Scala. The next version of the language should provide more flexible enumeration implementation with the possibility to for example define different properties.

Share, like or comment this post on Twitter:

Share on: