Sealed keyword in Scala

Versions: Scala 2.12.1

When we come to Scala and see the sealed keyword, we often wonder "why". After all, having all subclasses defined in one or more files shouldn't be a big deal. For us, programmers, it's not but for the compiler, it has an importance. In this post, I will try to show the sealed class use cases.

In the first section I will shortly explain the sealed classes in order to leave much more place to the use cases. I will cover each of them in a separate section.

Definition

The sealed is a Scala keyword used to control the places where given trait or class can be extended. More concretely, the subclasses and the implementations can be defined only in the same source file as the sealed trait or class.

Despite their simple definition, sealed types are used for some specific goals. And I will list them with examples in the next sections. Obviously, having all the classes in a single file can negatively impact the code readability. And that's the reason why it's good to see first whether the advantages of sealing don't break the balance with the readability.

Type safety

The first advantage of the sealed types is related to the type safety. If you carefully analyze all compilation warnings, you've probably already seen warning: match may not be exhaustive. Just to recall, they happen when your match cases don't cover all possibilities.

But what is the link with the sealed types? The mystery comes from the definition of the constraint. Since all the types must be defined in a single file, the compiler automatically knows all possibilities and can use them in every pattern matching executed against the sealed type. Let's take an example:

package sealedtypes

sealed trait Letter {}

class A extends Letter
class B extends Letter
class C extends Letter

If we try to extend Letter trait in a different file, the compiler will end up with "illegal inheritance from sealed trait Letter" message. And if we write the code like this one:

  val letter: Letter = new B()
  letter match {
    case _: sealedtypes.A => "a"
  }

The compiler will print:

Warning:(7, 3) match may not be exhaustive.
It would fail on the following input: C()
  letter match {

You can later remove the sealed keyword and recompile the code in order to see that the warning will disappear.

Algebraic Data Types

In simple terms, the Algebraic Data Types (I will refer to them with the ADT abbreviation) symbolize the data model with a closed set of possible values sharing the same interface. Probably the 2 most popular ADTs you'll meet are product and sum types. The best example illustrating the former one is a class composed of other types. On the other side, the sum type doesn't combine different types into one common type. Rather than that, it enumerates the list of the same types in a container, exactly as Java's enum does.

Since all the implementations of sealed must be defined in the same file as the declaration, they become a good candidate to be implemented as the sum type:

sealed trait Letter

object Letter {
  class A() extends Letter {}
  class B() extends Letter {}
  class C() extends Letter {}
}

Enum alternative

Enum drawbacks was already addressed in the Enums in Scala post. Just to recall quickly, one of the negative points was the inability to extend the enums behavior. Thanks to the sealed types, as I partially illustrated this in the previous section, it's possible to use more dynamic enumerations:

sealed trait Letter {
  def letter: String
  def isVowel: Boolean
}

object Letter {
  case object A extends Letter {
    override def letter: String = "A"

    override def isVowel: Boolean = true
  }
  case object B extends Letter {
    override def letter: String = "B"

    override def isVowel: Boolean = false
  }
  case object C extends Letter {
    override def letter: String = "C"

    override def isVowel: Boolean = false
  }
}

Despite their apparent simplicity, it's often hard to find the use cases of the sealed types. I hope I illustrated some of them in this post. From the examples, you can deduce that you can freely use sealed types when you're dealing with a finite set of possibilities. It's true and this use case is a good alternative to the classical Scala enums. However, you should always be aware of the eventual impact on the code readability.