As @lambdista mentioned on Twitter, it would make sense to remove the copy
operation from case classes that have a private constructor.
Here is a copy of his motivating example:
case class Nat private (value: Int) {
// Define a `copy` member so that the compiler does not synthesize one,
// and make it private so that users can not even call it
private def copy(): Unit = ()
}
object Nat {
def apply(value: Int): Option[Nat] =
if (value < 0) None else Some(new Nat(value))
}
Keeping the default public copy
operation would defeat the purpose of having a private constructor because it would make it possible to create an invalid instance.
As mentioned in the Twitter discussion, opaque types already fix the issue of creating a “newtype” with controlled constructors. However, I think case classes with private constructors cover use cases that are not covered by opaque types because case classes support pattern matching and can have multiple fields:
case class Rational private (num: Int, denom: Int)
object Rational {
def apply(num: Int, denom: Int): Option[Rational] =
if (denom == 0) None else Some(new Rational(num, denom))
}
For comparison, the same example with opaque types would awkwardly look like the following:
opaque type Rational = (Int, Int)
object Rational {
def apply(num: Int, denom: Int): Option[Rational] =
if (denom == 0) None else Some((num, denom))
implicit class Ops(`this`: Rational) extends AnyVal {
def num: Int = `this`._1
def denom: Int = `this`._2
}
}
I think this gives enough motivation for a better support of case classes with private constructors. What do you think? An auxiliary question could be: should we replace the syntax of opaque tyes with value classes with private constructors?
As suggested by @buzden, instead of removing the copy
operation, it would make more sense to give it the same visibility as the constructor. I’m not sure how to provide a smooth migration path for such a change, though.