Home > Software design >  Why does scala allow private case class fields?
Why does scala allow private case class fields?

Time:07-26

In scala, it is legal to write case class Foo(private val bar: Any, private val baz: Any).

This works like one might hope, Foo(1, 2) == Foo(1, 2) and a copy method is generated as well. In my testing, marking the entire constructor as private marks the copy method as private, which is great.

However, either way, you can still do this:

Foo(1, 2) match { case Foo(bar, baz) => bar } // 1

So by pattern matching you can extract the value. That seems to render the private in private val bar: Any more of a suggestion. I saw someone say that "If you want encapsulation, a case class is not the right abstraction", which is a valid point I reckon I agree with. But that raises the question, why is this syntax even allowed if it is arguably misleading?

CodePudding user response:

It's because case classes were not primary intended to be used with private constructors or fields in the first place. Their main purpose is to model immutable data. As you saw there are workarounds to get the fields so using a private constructor or private fields on a case class is usually a sign of a code smell.

Still, the syntax is allowed, because the code is syntactically and semantically correct - as far as the compiler is concerned. But from the programmer's point of view is at the limit of "does it makes sense to be used it like that?" Probably not.

Marking the constructor of a case class as private does not have the effect you want, and it does not make the copy method private, at least not in Scala 2.13.

case class Foo private (bar: Int, baz: Int)

The only thing I can't do is this:

val foo1 = new Foo(1, 2)   // no constructor accessible from here

But I can do this:

  val foo2 = Foo(1, 2)       // ok
  val foo3 = Foo.apply(1, 2) // ok
  val foo4 = foo2.copy(4)    // ok - Foo(4,2)

A case class as it's name implies means the object is precisely intended to be pattern matched or "caseable" - that means you can use it like this:

case Foo(x, y) =>       // do something with x and y

Or this:

val foo2 = Foo(1, 2)
val Foo(x, y) = foo2    // equivalent to the previous

Otherwise why would you mark it as a case class instead of a class ? Sure, one could argue a case class is more convenient because it comes with a lot of methods (rest assured, all that comes with an overhead at runtime as a trade-off for all those created methods) but if privacy is what you are after, they don't bring anything to the table on that.

A case class creates a companion object with an unapply method inside which when given an instance of your case class, it will deconstruct/destructure it into its initialized fields. This is also called an extractor method.

The companion object and it's class can access each other's members - that includes private constructor and fields. That is how it was designed. Now apply and unapply are public methods defined on the companion object which means - you can still create new objects using apply, and if your fields are private - you can still access them from unapply.

Still, you can overwrite them both in the companion object if you really want your case class to be private. Most of the times though, it won't make sense to do so, unless you have some really specific requirements:

  case class Foo2 private (private val bar: Int, private val baz: Int)

  object Foo2 {
    private def apply(bar: Int, baz: Int) = new Foo2(bar, baz)
    private def unapply(f: Foo2): Option[(Int, Int)] = Some(f.bar, f.baz)
  }

  val foo11                         = new Foo2(1, 2)   // won't compile
  val foo22                         = Foo2(1, 2)       // won't compile
  val foo33                         = Foo2.apply(1, 2) // won't compile
  val Foo2(gotya: Int, gotya2: Int) = foo22            // won't compile
   
  println(Foo2(1, 2) == Foo2(1, 2))                    // won't compile

  val sum = foo22 match {
    case Foo2(x, y) => x   y                           // won't compile
  }

But I will still be able to see the contents of Foo2 by printing it, because case classes also overwrite toString and while you can't make that private, you can still overwrite it to print something else. I will leave that to you to try out.

 print(foo11)  // Foo2(1,2)

CodePudding user response:

Adding to the previous answer: you can make the copy method inaccessible by adding a private method named copy:

case class Foo3(private val x: Int) {
  private def copy: Foo3 = this
}

Foo3(1).copy(x = 2) // won't compile ("method ... cannot be accessed")
  • Related