Scala - Enumerations
Scala 2's `scala.Enumeration` exists primarily for Java interoperability. It uses runtime reflection and lacks compile-time type safety.
Key Insights
- Scala provides three enumeration approaches: legacy
scala.Enumeration, Scala 3’s nativeenum, and sealed trait hierarchies, each with distinct trade-offs for type safety and pattern matching - Scala 3 enums compile to sealed classes with singleton instances, offering algebraic data type (ADT) capabilities that far exceed simple value lists
- For production systems, sealed traits provide maximum flexibility and type safety in Scala 2, while Scala 3 enums deliver cleaner syntax with equivalent power
Legacy Scala Enumeration
Scala 2’s scala.Enumeration exists primarily for Java interoperability. It uses runtime reflection and lacks compile-time type safety.
object Color extends Enumeration {
type Color = Value
val Red, Green, Blue = Value
}
import Color._
def describeColor(color: Color): String = color match {
case Red => "warm"
case Green => "natural"
case Blue => "cool"
// Warning: match may not be exhaustive
}
// Usage
val myColor: Color = Color.Red
println(Color.values) // Set(Red, Green, Blue)
println(myColor.id) // 0
The fundamental problem: the compiler cannot verify pattern match exhaustiveness. Add a new color, and existing matches won’t generate warnings.
object Status extends Enumeration {
val Pending, Active, Completed, Failed = Value
// Custom values and names
val Archived = Value(10, "ARCHIVED")
def fromString(s: String): Option[Value] =
values.find(_.toString == s)
}
// Type erasure issues
def process(status: Status.Value): Unit = {
// status.type is just Enumeration.Value at runtime
}
Avoid scala.Enumeration for new code. It’s a pre-2.10 design that predates modern Scala idioms.
Sealed Trait Hierarchies (Scala 2 & 3)
Sealed traits provide compile-time exhaustiveness checking and full type system integration.
sealed trait HttpMethod
case object GET extends HttpMethod
case object POST extends HttpMethod
case object PUT extends HttpMethod
case object DELETE extends HttpMethod
case object PATCH extends HttpMethod
def requiresBody(method: HttpMethod): Boolean = method match {
case POST | PUT | PATCH => true
case GET | DELETE => false
// Compiler error if we miss a case
}
// Type-safe pattern matching
def routeRequest(method: HttpMethod, path: String): Unit = {
method match {
case GET => handleGet(path)
case POST => handlePost(path)
case PUT => handlePut(path)
case DELETE => handleDelete(path)
case PATCH => handlePatch(path)
}
}
Add parameters for richer ADTs:
sealed trait Result[+T]
case class Success[T](value: T) extends Result[T]
case class Failure(error: String, code: Int) extends Result[Nothing]
case object Pending extends Result[Nothing]
def processResult[T](result: Result[T]): Option[T] = result match {
case Success(value) => Some(value)
case Failure(err, code) =>
println(s"Error $code: $err")
None
case Pending => None
}
// Usage with type inference
val result: Result[Int] = Success(42)
processResult(result) // Some(42)
Sealed traits with case objects provide enumeration-like behavior while maintaining full type safety:
sealed trait LogLevel {
def priority: Int
}
case object Debug extends LogLevel { val priority = 0 }
case object Info extends LogLevel { val priority = 1 }
case object Warn extends LogLevel { val priority = 2 }
case object Error extends LogLevel { val priority = 3 }
object LogLevel {
val all: Set[LogLevel] = Set(Debug, Info, Warn, Error)
def fromString(s: String): Option[LogLevel] = s.toUpperCase match {
case "DEBUG" => Some(Debug)
case "INFO" => Some(Info)
case "WARN" => Some(Warn)
case "ERROR" => Some(Error)
case _ => None
}
}
def shouldLog(current: LogLevel, minimum: LogLevel): Boolean =
current.priority >= minimum.priority
Scala 3 Native Enums
Scala 3 introduces first-class enum support with clean syntax and ADT capabilities.
enum Direction:
case North, South, East, West
def opposite(dir: Direction): Direction = dir match
case Direction.North => Direction.South
case Direction.South => Direction.North
case Direction.East => Direction.West
case Direction.West => Direction.East
// Enums with parameters
enum Color(val rgb: Int):
case Red extends Color(0xFF0000)
case Green extends Color(0x00FF00)
case Blue extends Color(0x0000FF)
println(Color.Red.rgb) // 16711680
Enums compile to sealed classes with case objects, providing the same guarantees as manual sealed trait hierarchies:
enum HttpStatus(val code: Int, val message: String):
case Ok extends HttpStatus(200, "OK")
case Created extends HttpStatus(201, "Created")
case BadRequest extends HttpStatus(400, "Bad Request")
case Unauthorized extends HttpStatus(401, "Unauthorized")
case NotFound extends HttpStatus(404, "Not Found")
case ServerError extends HttpStatus(500, "Internal Server Error")
def isSuccess(status: HttpStatus): Boolean =
status.code >= 200 && status.code < 300
// Pattern matching with guards
def handleResponse(status: HttpStatus): String = status match
case s if s.code < 300 => s"Success: ${s.message}"
case s if s.code < 500 => s"Client error: ${s.message}"
case s => s"Server error: ${s.message}"
Enums support methods and companion object utilities:
enum Operation:
case Add, Subtract, Multiply, Divide
def symbol: String = this match
case Add => "+"
case Subtract => "-"
case Multiply => "*"
case Divide => "/"
def apply(a: Int, b: Int): Int = this match
case Add => a + b
case Subtract => a - b
case Multiply => a * b
case Divide => a / b
object Operation:
def fromSymbol(s: String): Option[Operation] = s match
case "+" => Some(Add)
case "-" => Some(Subtract)
case "*" => Some(Multiply)
case "/" => Some(Divide)
case _ => None
// Usage
val op = Operation.Multiply
println(op.symbol) // *
println(op(6, 7)) // 42
ADTs with Enums
Scala 3 enums excel at modeling algebraic data types:
enum Json:
case JNull
case JBoolean(value: Boolean)
case JNumber(value: Double)
case JString(value: String)
case JArray(elements: List[Json])
case JObject(fields: Map[String, Json])
def stringify(json: Json): String = json match
case Json.JNull => "null"
case Json.JBoolean(b) => b.toString
case Json.JNumber(n) => n.toString
case Json.JString(s) => s"\"$s\""
case Json.JArray(elems) => elems.map(stringify).mkString("[", ",", "]")
case Json.JObject(fields) =>
fields.map((k, v) => s"\"$k\":${stringify(v)}").mkString("{", ",", "}")
// Example usage
val data = Json.JObject(Map(
"name" -> Json.JString("Alice"),
"age" -> Json.JNumber(30),
"active" -> Json.JBoolean(true)
))
println(stringify(data))
// {"name":"Alice","age":30.0,"active":true}
Enums integrate with generics and variance:
enum Validation[+E, +A]:
case Valid(value: A) extends Validation[Nothing, A]
case Invalid(errors: List[E]) extends Validation[E, Nothing]
def map[B](f: A => B): Validation[E, B] = this match
case Valid(a) => Valid(f(a))
case Invalid(e) => Invalid(e)
def flatMap[E2 >: E, B](f: A => Validation[E2, B]): Validation[E2, B] =
this match
case Valid(a) => f(a)
case Invalid(e) => Invalid(e)
// Validation example
def validateAge(age: Int): Validation[String, Int] =
if age >= 0 && age <= 150 then Validation.Valid(age)
else Validation.Invalid(List("Age must be between 0 and 150"))
def validateName(name: String): Validation[String, String] =
if name.nonEmpty then Validation.Valid(name)
else Validation.Invalid(List("Name cannot be empty"))
Serialization and Java Interop
For JSON serialization with libraries like Circe or Play JSON, sealed traits and enums work seamlessly:
// Scala 3 enum with Circe
import io.circe.{Encoder, Decoder}
import io.circe.generic.semiauto._
enum Priority:
case Low, Medium, High, Critical
object Priority:
given Encoder[Priority] = Encoder.encodeString.contramap(_.toString)
given Decoder[Priority] = Decoder.decodeString.emap { str =>
Priority.values.find(_.toString == str)
.toRight(s"Invalid priority: $str")
}
For Java interoperability, use explicit ordinal methods:
enum Status:
case Pending, Active, Completed
def ordinal: Int = this.ordinal
object Status:
def fromOrdinal(i: Int): Option[Status] =
Status.values.lift(i)
// Java-friendly API
class StatusAPI:
def getStatusCode(status: Status): Int = status.ordinal
def getStatusFromCode(code: Int): Status =
Status.fromOrdinal(code).getOrElse(Status.Pending)
Practical Recommendations
Use Scala 3 enums for new Scala 3 projects—they provide clean syntax with full type safety. Use sealed trait hierarchies in Scala 2 or when you need maximum flexibility. Avoid scala.Enumeration unless maintaining legacy code or requiring Java enum compatibility.
For domain modeling, prefer enums with parameters over simple value lists. The type system catches errors at compile time, and pattern matching becomes self-documenting code.