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?
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.
LATER EDIT: In Scala 3, marking the constructor as private, does make the apply and copy methods private. This change was also developed for Scala 2 and can be found here - but it was delayed for the 2.14 future release. The reason it can't go into Scala 2.13 is because it's a breaking change.
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(bar, baz) = 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
}
Still, you can see the contents of Foo2 by printing it, because case classes also overwrite toString and you can't make that private, so you'll have to overwrite it to print something else. I will leave that to you to try out.
print(foo11) // Foo2(1,2)
As you see, a case class brings multiple access points to it's constructor and fields. This example was just for understanding the concept. It is not an example of a good design. Usually in OOP, you need an instance of some class, to perform operations on it. So a class that you cannot instantiate at all is no more useful than a Scala object. If you find yourself blocking all ways to create an instance of some class or case class, that's a sign you probably need an object instead since object is already a singleton in Scala.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With