Proposal to retain private[this] in Scala 3

Er …

$ scala
Welcome to Scala 2.12.8 (OpenJDK 64-Bit Server VM, Java 1.8.0_222).
Type in expressions for evaluation. Or try :help.

scala> :paste
// Entering paste mode (ctrl-D to finish)

case class PositiveInt(private val int: Int) extends AnyVal

object PositiveInt {
  def apply(int: Int): Option[PositiveInt] =
    if (int > 0)
      Some(new PositiveInt(int))
    else
      None
}

// Exiting paste mode, now interpreting.

defined class PositiveInt
defined object PositiveInt

scala> PositiveInt(-3)
res0: Option[PositiveInt] = None

scala> new PositiveInt(-3)
res1: PositiveInt = PositiveInt(-3)
1 Like

The private here is package private so it won’t work in a repl since you have a global package where everything is visible.

The idea is that you define PositiveInt in its own package in some library where your code wouldn’t typically sit inside.

Also its meant to be

case class PositiveInt private (int: Int) extends AnyVal

I got the syntax wrong in my previous post, just checked the code.

Hum, you are probably misled, here. private means enclosing-class/object-private. In Scala it never means package-private.

OK let’s use your fixed definition and break it anyway:

$ scala
Welcome to Scala 2.12.8 (OpenJDK 64-Bit Server VM, Java 1.8.0_222).
Type in expressions for evaluation. Or try :help.

scala> :paste
// Entering paste mode (ctrl-D to finish)

case class PositiveInt private (int: Int) extends AnyVal

object PositiveInt {
  def apply(int: Int): Option[PositiveInt] =
    if (int > 0)
      Some(new PositiveInt(int))
    else
      None
}

// Exiting paste mode, now interpreting.

defined class PositiveInt
defined object PositiveInt

scala> PositiveInt(-3)
res0: Option[PositiveInt] = None

// the constructor is indeed *not* package-private; I can't access it now:
scala> new PositiveInt(-3)
<console>:14: error: constructor PositiveInt in class PositiveInt cannot be accessed in object $iw
       new PositiveInt(-3)
       ^

scala> PositiveInt(3).get
res4: PositiveInt = PositiveInt(3)

// it doesn't matter, though; I can still use `copy` to break the invariant
scala> PositiveInt(3).get.copy(-3)
res5: PositiveInt = PositiveInt(-3)

I really don’t think you want your PositiveInt to be a case class at all. You probably want a regular class.

1 Like

It’s OK, we can fix it by using a sealed abstract case class :grin: hmm.md · GitHub

1 Like

Or we can properly fix it.

4 Likes

Or seriously, you can make it non-case! The only advantage of a case class that is used in this example is the auto-generated toString(). You can equally well declare it explicitly, and not inherit all the problems that a case class brings with it.

Don’t use case classes unless you actually intend to match on them.

1 Like

Well this was obviously not the best example, both of the requested use case (case classes with private parameters), and of the intended use case (case classes with private constructors)…

I don’t know what is going on, but here is a literal code that we have

sealed abstract class Barcode extends Serializable with Product {
  val asString: String
}


object Barcode {
  final case class Ean private (asString: String) extends Barcode {
    def copy(asString: String): Ean =
      if (asString.trim.length == 0)
        throw new IllegalArgumentException(s"Invalid Ean $asString")
      else {
        val padded = Ean.leftPad(asString, '0', 13)
        padded match {
          case Ean.regex(_*) => new Ean(padded)
          case _             => throw new IllegalArgumentException(s"Invalid Ean: $asString")
        }
  }

  object Ean {
    val regex: Regex = """[0-9]{13}""".r

    @tailrec
    private[model] def leftPad(s: String, c: Char, target: Int): String =
      if (s.length < target) leftPad(c + s, c, target) else s

    def apply(value: String): Option[Ean] =
      if (value.trim.length == 0)
        None
      else {
        val padded = leftPad(value, '0', 13)
        padded match {
          case regex(_*) => Some(new Ean(padded))
          case _         => None
        }
      }
  }
}

With the following

object Test {
  /*
  This gives compile error `constructor Ean in class Ean cannot be accessed in object Test`
  [error]   val test = new Barcode.Ean("test")
  */
  val test = new Barcode.Ean("test")

  // This is of type Option[Barcode.Ean] which is the point. The value in this case will be `None`
  val test2 = Barcode.Ean("test") 
}

Maybe the sealed is making a difference here? In any case we are using this pattern everywhere.

1 Like

Isn’t the issue matthew is facing safe construction, rather than matching? Matching and extracting a value from the case class is fine, he just doesn’t want you to be able to construct one with contents that are invalid, and to force everyone to go through a factory that validates things

2 Likes

Exactly, anyways I think this is going off topic a bit…

I think your last amended proposal is the right one with MyClass being replaced by This suggested by @kjsingh

I have realized that the loss of the user-facing semantics of private[this] in Scala 3, while regrettable, would be merely inconvenient for me, whereas the loss of private[MyScope] would damage encapsulation in my project.

private[MyScope] will be unchanged in Scala 3, right?

I recently wrote some code like this

final object Foo {
  trait Bar {
    private[this] final val myState = ???
    private[Foo] final def bar() = ??? // uses this.myState
  }
  def fooPublic(b: Bar) =  ??? // uses b.bar()
}

The use of private[Foo] in this code was very useful to restrict the calling of Bar.bar to within Foo. One might think to put fooPublic in the Bar companion and drop use of private[Foo], but in this case fooPublic was a broader, unrelated operation that merely called into Bar.

Also, regarding the “Amended, specific proposal” from above, it occurs to me that given

  1. Scala 2’s private[this] becomes private in Scala 3

This may result in some user confusion when a variable marked private isn’t accessible in the companion. That variable would need to be marked private[MyObject] to become accessible in the companion, which may add an element of non-obviousness.

Personally my latest opinion is I’d be glad if Scala 3 kept private[this]. The “Amended, specific proposal” still seems like a reasonable way to deprecate private[this].

1 Like