9

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?

Emil Lundberg
  • 7,268
  • 6
  • 37
  • 53
  • 2
    Whether or not sealing your hierarchy is a "good practice" is largely subjective to the situation at hand. To answer this question, one would have to know more about what your hierarchy is meant to represent and how it is going to be used. – whaley Jun 30 '15 at 11:33

3 Answers3

11

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.

kiritsuku
  • 52,967
  • 18
  • 114
  • 136
  • 2
    `Making them sealed is only useful when you want exhaustiveness checking on its subclasses, which is not a very likely use case.` That's what I was looking for, and also what I concluded after thinking about it for a bit. Exhaustivity on the (sealed) base trait can still be guaranteed if all the inheriting case classes are handled, since any instance of any subclass of those case classes is also by definition an instance of one of those case classes. – Emil Lundberg Jul 01 '15 at 11:24
2

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.
Puneeth Reddy V
  • 1,538
  • 13
  • 28
  • I don't understand what you're trying to say. What do you mean by `default class`, `In sealed classes all the cases are executed even after a exception is raised for any unknown case` and `if a case is unknown for non-sealed classes(even default fails) then further cases are stopped`? – Emil Lundberg Jul 01 '15 at 11:26
  • I'm sorry it is my mistake to give a statement like that. – Puneeth Reddy V Jul 01 '15 at 12:44
  • `So, is it good practice to seal the case classes as well?` does it mean that are you willing to add to sealed keyword to `Assign`, `Print`, `Zero`, `Block` classes OR you are going to add those classes into `sealed trait Statement` file.? – Puneeth Reddy V Jul 01 '15 at 12:53
  • The former. I had already sealed the supertrait, and was considering whether to also seal the inheriting case classes. – Emil Lundberg Jul 01 '15 at 22:16
  • 1
    Consider the Cons if you have simple to code maintain it is left to you. `So, is it good practice to seal the case classes as well?` No its not a good practice to seal the case classes we should use sealed only for the base class or trait. `What could go wrong if I don't?` If do so complier thinks all the possible classes that could appear in the match case( looks for a child class of a `Assign` , `Print` etc ). Still you will get same result. – Puneeth Reddy V Jul 02 '15 at 05:41
0

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.

lmm
  • 17,386
  • 3
  • 26
  • 37