1. Overview

A final modifier in Scala makes a class or trait unavailable for an extension. Therefore, it’s quite restrictive. On the other hand, making a class public allows any other class to extend from it.

What if we want something in between these two? The sealed modifier in Scala comes to the rescue!

In this tutorial, we’ll learn what the sealed keyword is in Scala, what its uses are, and how to use it properly.

2. What Is the sealed Keyword?

The sealed keyword is used to control the extension of classes and traits. Declaring a class or trait as sealed restricts where we can define its subclasses — we have to define them in the same source file.

In the next section, we’ll see the characteristics and behavior of the sealed modifier, such as the compiler warnings and exceptions we’ll encounter when using sealed in various scenarios.

3. Characteristics of sealed

Let’s see the implementation of a multiple-choice options class using sealed in the source file SealedClassExample.scala:

sealed abstract class MultipleChoice

case class OptionA() extends MultipleChoice
case class OptionB() extends MultipleChoice
case class OptionC() extends MultipleChoice

If we try to extend a sealed trait outside of the parent class file, the compiler will throw an exception with a message:

illegal inheritance from sealed class.

Let’s create another source file named SelaedExtendedDifferentFile.scala and try some experiments:

case class OptionD() extends MultipleChoice
Error:(5, 32) illegal inheritance from sealed class MultipleChoice
case class OptionD() extends MultipleChoice

However, there is no restriction in extending from a subclass of a sealed class:

case class OptionY() extends OptionX  //This is valid

This secondary subclass may be used in pattern matching, and the compiler will not give any warning if we do not include in the match cases:

case class OptionY() extends OptionX

def selectOption(option: MultipleChoice): String = option match {
  case optionY: OptionY => "Option-Y Selected"
  case optionX: OptionX => "Option-X Selected"
}

println(selectOption(OptionY()))

The print statement executes without any exceptions, even though OptionY is a subclass of MultipleChoice and we defined it in another source file:

Option-Y Selected

We can prevent this situation by using a case-class, which doesn’t allow inheriting from it.

Scala allows using the sealed modifier for traits, classes, and abstract classes. In the context of sealed, a class, trait, or abstract class functions the same except for the normal differences between classes and traits — for instance, an abstract class can receive parameters, and traits cannot.

4. Advantages of Using sealed

Using sealed classes, we can guarantee that only subclasses defined in the file exist. This helps the compiler know all the subclasses of the sealed class. Therefore, this behavior is useful in scenarios like pattern matching.

The compiler can emit warnings in case the match cases are not exhaustive, to prevent a MatchError exception. Let’s try to omit OptionC from our pattern match and observe the compiler behavior:

def selectOption(option: MultipleChoice): String = option match {
  case optionA: OptionA => "Option-A Selected"
  case optionB: OptionB => "Option-B Selected"
}

We get a nice warning message from the compiler because of non-exhaustive match cases:

Warning:(11, 54) match may not be exhaustive.
It would fail on the following input: OptionC()
  def selectOption(option: MultipleChoice): String = option match {

5. Using sealed as an Alternative for Enum

Enumerations in Scala have certain drawbacks, such as their inability to extend the enum behavior. Enums have the same type after an erasure. There is no exhaustive match checking during compile time. We can overcome this problem using a set of sealed case objects. Using sealed provides us the flexibility to keep it simple or add more features if required.

Let’s see how we can implement a DaysOfTheWeek enumeration using a sealed class:

sealed abstract class DayOfTheWeek(val name: String, val isWeekEnd: Boolean)

case object Monday extends DayOfTheWeek("Monday", false)
case object Tuesday extends DayOfTheWeek("Tuesday",  false)
case object Wednesday extends DayOfTheWeek("Wednesday", false)
case object Thursday extends DayOfTheWeek("Thursday", false)
case object Friday extends DayOfTheWeek("Friday", false)
case object Saturday extends DayOfTheWeek("Saturday", true)
case object Sunday extends DayOfTheWeek("Sunday", true)

However, it’s worth noting that there are some disadvantages to this approach when compared with Scala’s Enumeration class. For example, there are no default methods available for serialization/de-serialization, ordering of values, or retrieving a list of all values.

Therefore, we must evaluate our use case better and choose the approach that’s most appropriate for the situation.

6. Sealed as Algebraic Data Type (ADT)

Algebraic Data Type is a kind of composite type formed by a fixed set of possible values implementing a standard interface. The main characteristic of an ADT is its capability to make illegal states impossible to exist. There are two major ADTs: product types and sum types. The sum type lists all possible values of a type in a container.

Let’s see the example of a coffee shop implementing types of coffee as a sum type ADT using a sealed trait:

sealed trait Coffee

case object Cappuccino extends Coffee
case object Americano extends Coffee
case object Espresso extends Coffee

7. Conclusion

In this tutorial, we’ve learned what the sealed keyword is in Scala. We’ve seen its characteristics and advantages in pattern matching and its applications as enums and ADTs. Also, we’ve learned where and how to use it properly in our Scala applications.

As always, the full source code for the examples is available over on GitHub.


» 下一篇: Scala中的提升