47 Degrees joins forces with Xebia read more

Enumerations in Scala 3

Enumerations in Scala 3

This is the first post looking at a few of the new and updated features coming to Scala 3. Today, we are going to be looking at enumerations and how they have improved from the approaches taken in Scala 2.

Scala 2 had enumerations:

The intention with Scala 2 was to extends the Enumeration class, giving your enumeration name and values in the enclosing definition:

object Directions extends Enumeration {
  type Direction = Value

  val North, South, East, West = Value

  def f(d: Direction): Boolean = d == North
}

There were a few issues with this. In this example, North, South, East, and West aren’t full first-class citizens: when used in pattern matching, you are not warned for non-exhaustive checks, and they lose their underlying type after compilation, making method overloading pointless, if not impossible.

These enumerated values are val fields, so you cannot do any more than you can do with a Java Enum:

public enum Die {
    One(1),
    Two(2),
    Three(3),
    Four(4),
    Five(5),
    Six(6);

    private int faceValue;

    Die(int faceValue) {
        this.faceValue = faceValue;
    }

    public int getFaceValue() {
        return faceValue;
    }
}


The above simply is not possible using the packaged Scala 2 enumeration approach.

There are a couple of ways around this:

Sealed hierarchies

You can create a sealed trait (or sealed abstract class) and provide all your possible instances:

sealed trait Direction
case object North extends Direction
case object South extends Direction
case object East extends Direction
case object West extends Direction

These directions are “plain old” Scala objects - there is nothing surprising here. Similarly, you can add instance values to your Direction type, so implementing the Die type is straightforward.

Enumeratum library

The Enumeratum library is another approach to providing better enumeration support in Scala 2. It bridges the gap between the roll-your-own sealed trait approach, providing many of the Enumeration class features at compile-time, such as the index value of the type, or utility methods for moving to and from the instances based on their instance name:

import enumeratum._

sealed trait Direction

object Direction extends Enum[Direction] {
  val values = findValues

  case object North extends Direction
  case object South extends Direction
  case object East extends Direction
  case object West extends Direction
}

Direction.withName("East")
// res0: Direction = East

It does suffer from some of the boilerplate seen with the regular Scala Enumeration too.

Scala 3: Introducing the new enum keyword

Scala 3 has introduced a new enum keyword. Our direction enumeration can now be constructed like this:

enum Direction:
  case North extends Direction
  case South extends Direction
  case East extends Direction
  case West extends Direction

We have removed most of the boilerplate and unintuitive syntax: we are not extending a class, we don’t need to assign a type to the value, and we don’t need to assign our values.

Similarly, we can add instance data to the enumerated instances:

enum Die(val faceValue: Int):
  case One extends Die(1)
  case Two extends Die(2)
  case Three extends Die(3)
  case Four extends Die(4)
  case Five extends Die(5)
  case Six extends Die(6)

As with the original Enumeration implementation, these new values come with many helpers, such as extracting all of the values, or a particular index:

scala> val n = Direction.North
val n: Direction = North

scala> val six = Die.valueOf("Six")
val six: Die = Six

scala> val first = Die.fromOrdinal(0)
val first: Die = One

It is fine to work with the enum similar to how you would with a class, object, or trait: adding members in the form of val and def is allowed.

Algebraic Data Types, too

A nice addition is that we can use the enum to create ADT sum types, much like we would have done with the sealed trait hierarchy:

enum BinaryTree[+A]:
  case Node(value: A, left: BinaryTree[A], right: BinaryTree[A])
  case Leaf

Note here that we do not need to explicitly state that Node and Leaf extend BinaryTree.

In our original Direction enumeration, we could have omitted the extends Direction part.

scala> val tree = BinaryTree.Node("Hello", BinaryTree.Leaf, BinaryTree.Leaf)
val tree: BinaryTree[String] = Node(Hello,Leaf,Leaf)

scala> val leaf = BinaryTree.Leaf
val leaf: BinaryTree[Nothing] = Leaf

Similarly, Leaf extends BinaryTree[Nothing]: it does not need to be explicitly stated in the enum definition.

The new enum keyword is a welcome addition to the language, and should make for clearer code without the boilerplate gymnastics often seen in Scala 2.

Keep a look out on our blog and Twitter account for more blog posts soon about new and improved features in Scala 3. We will have a few more posts in the coming weeks as Scala 3 is officially launched.

Ensure the success of your project

47 Degrees can work with you to help manage the risks of technology evolution, develop a team of top-tier engaged developers, improve productivity, lower maintenance cost, increase hardware utilization, and improve product quality; all while using the best technologies.