1

I recently came across the following code pattern in Scala:

sealed trait ConfirmAction
object ConfirmAction {
  final case object NewGame extends ConfirmAction
  final case object Quit    extends ConfirmAction
}

I understand that this defines subtypes of the ConfirmAction sealed trait as case objects within the companion object. However, I'm curious about a couple of things:

  1. Why are the case objects NewGame and Quit marked as final? Isn't this redundant since case objects are inherently singletons and can't be extended?

  2. Is there a specific reason for choosing this pattern of defining subtypes within the companion object, rather than declaring them directly under the sealed trait itself, like:

sealed trait ConfirmAction
final case object NewGame extends ConfirmAction
final case object Quit extends ConfirmAction

Additionally, I'd like to know more about this approach and its benefits. Can you please explain the rationale behind these choices and point me to resources where I can learn more about this way of declaring subtyping patterns?

Thank you!

looking around and googling for this specific implementation of the domain model in google.

Gaël J
  • 11,274
  • 4
  • 17
  • 32
  • 3
    The nesting is mostly for style, so you access them as `ConfirmAction .NewGame` rather than just `NewGame`, for some that may read better; also, it reduces nome pollution. - The `final` was because of a _"compiler bug"_ where adding the `final` applied some optimizations to the generated bytecode, as **Scala 3** is not longer needed not possible to add `final` to an `object` – Luis Miguel Mejía Suárez Aug 25 '23 at 15:29
  • 2
    Declaring these in the companion object is easier in terms of "scoping" IMHO. When you don't know the values, you just go to the companion object and find them. And when there are multiple traits, it clarified from which one a value comes from, less risk of collision as well. – Gaël J Aug 25 '23 at 16:48
  • And if you want to use them without namespaces, you can `import ConfirmAction._` – Daenyth Aug 30 '23 at 17:43

1 Answers1

1

Both of the code samples you give are valid syntax--which one you choose is a matter of preference. In the official docs, the syntax example is more similar to your second example:

sealed trait Message
case class PlaySong(name: String) extends Message
case class IncreaseVolume(amount: Int) extends Message
case class DecreaseVolume(amount: Int) extends Message
case object StopPlaying extends Message

By nesting the case objects within another object, you can refer to them like ConfirmAction.NewGame, which could be convenient if you use the name NewGame elsewhere in the codebase.

The final keyword is not necessary, but perhaps was added to emphasize that it doesn't make sense to extend case objects. Some developers prefer to mark all case classes as final (eg. this and this)--your example code seems to extend that logic to a case object.

That said, your code would compile and work correctly if the final keyword was removed.

Porter James
  • 188
  • 1
  • 8