Emil Lundberg
Emil Lundberg

Reputation: 7380

Is it good practice to make case classes sealed?

The main reason to seal classes seems to be that this allows the compiler to do exthaustivity searches when pattern matching on those classes. Say I have data types meant for pattern matching. Toy example:

sealed trait Statement
case class Assign(name: String, value: Int) extends Statement
case class Print(name: String) extends Statement
case class IfZero(name: String, thenn: Statement, els: Option[Statement]) extends Statement
case class Block(statements: List[Statement]) extends Statement

The use case for these classes would be to consume them through pattern matching:

def execute(statement: Statement): Unit = statement match {
    case Assign(name, value)      => ???
    case Print(name)              => ???
    case IfZero(name, thenn, els) => ???
    case Block(statements)        => statements foreach { execute(_) }
  }

To this end, the Statement trait is sealed so that the compiler can warn me if I forget one statement kind in the match statement. But what about the case classes? Case classes cannot inherit from each other, but traits and ordinary classes can. So, is it good practice to seal the case classes as well? What could go wrong if I don't?

Upvotes: 9

Views: 8486

Answers (3)

Puneeth Reddy V
Puneeth Reddy V

Reputation: 1568

This answer is left to the programmer.
Not all the times it is possible to define reasonable behaviour for default class.

The alternative to these types of problem is to have sealed class(common base class).

Pros of sealed class

  • We can use a sealed class if we are not able to generalise a default case.

  • During usage of case classes no default is necessary, since we cover all possibilities. If you miss any one we will get compilation warning match may not be exhaustive. In sealed classes also if a case is not present then we get "scala.MatchError:".

Cons of sealed class

  • Avoid using sealed case class hierarchy if the hierarchy changes frequently. Since the entire hierarchy has to be declared in the same file it can be costly to modify existing code,reset it(other code that uses it), and redeploy it.

Upvotes: 2

lmm
lmm

Reputation: 17431

I occasionally find it useful to extend a case class to enable specialized handling for a particular subset of instances:

case class Ellipse(major: Int, minor: Int) {
  def draw() = //general drawing code
}
class Circle(radius: Int) extends Ellipse(radius, radius) {
  override def draw() = //faster specialized code for drawing circles
}

We can also use these for pattern matches:

def draw(e: Ellipse) = {
  case c: Circle => //fast path for drawing a circle
  case _ => //general case - note that this case must be able to handle
            //ellipses that happen to be circular
}

This is legitimate as long as the subclass complies with LSP.

How important is this kind of thing? Probably not very. In your own application code it's probably fine to "default" to sealing all your case classes, since you can always "unseal" them. In a library I'd err on the side of leaving things unsealed, in case one of your users wants to do something like the above.

Upvotes: 0

kiritsuku
kiritsuku

Reputation: 53348

You don't have to seal the case classes but you should mark them as final and therefore forbid any further inheritance relationship. Making them sealed is only useful when you want exhaustiveness checking on its subclasses, which is not a very likely use case.

Marking all classes as final by default is a good thing because it forbids the users of your API to change the behavior of these classes when they override its methods. If you didn't specifically design your class to be subclassed, it may happen that the subclassing leads to bugs in your application because the subclassed class is no longer doing what it was intended to do.

Upvotes: 12

Related Questions