The power of sealed traits in Scala

Having improved a piece of code today by adding compile-time safety, I felt obliged to write a short explanation of sealed traits. For the Scala veterans, this will hopefully not be anything new.

Sealed traits can only be extended in the same files as they are declared – long story short, the JVM allows code to be loaded at runtime, so the compiler can’t scan the entire program to collect all of the subtypes. This ensures that the compiler knows about all the subtypes that extend the trait.

So what advantage does this limitation give us?

Take a look at the following code snippet and consider why it’s unsafe:

trait Color

case object Red extends Color
case object Blue extends Color
case object Green extends Color

def composePoem(color: Color): String = {
  color match {
    case Red => "Roses are red"
    case Blue => "Violets are blue"
  }
}

That’s right! We’re not handling the case of the color being Green in our match clause. This also means that, if in the future we decide to add another color, our composePoem function will break at runtime with a MatchError:

scala> composePoem(Green)
scala.MatchError: Green (of class Green$)
  at .composePoem(<console>:17)
  ... 36 elided

…Sealed trait to the rescue!

sealed trait Color

case object Red extends Color
case object Blue extends Color
case object Green extends Color

def composePoem(color: Color): String = {
  color match {
    case Red => "Roses are red"
    case Blue => "Violets are blue"
  }
}

By adding the sealed keyword, the compiler performs exhaustiveness checking and gives us a warning that the function would fail on the input “Green”:

<console>:15: warning: match may not be exhaustive.
It would fail on the following input: Green
         color match {
         ^

The compiler will thus tell us about every place in our code base that needs to be updated if we add more objects or classes that extend the sealed trait. We thus have stronger guarantees that our code is correct.

TL;DR: Use sealed traits to enable exhaustive matching. Issues highlighted at compile time are way better than issues highlighted at runtime.