The primary constructor of a class can be defined as private.
But it is not private at the level of bytecode, which is what MiMa works with.
Thus changes in such private primary constructor is deemed breaking by MiMa. But these changes are invisible to regular Scala code, so it shouldn’t break. But again, the constructor is visible to Java code which could break.
What to do with it?
Keep things as they are
“Fix” MiMa, so that it doesn’t mark private constructors as breaking
Find a way to make the primary constructor private even at the bytecode level
Anything else?
This issue came up when discussing how to evolve case classes without breaking backward compatibility.
scala:main
← sideeffffect:patch-1
opened 04:01PM - 21 Dec 22 UTC
Documenting the patter coming from _SIP-50 - Struct Classes_ https://github.com/… scala/improvement-proposals/pull/50#issuecomment-1361474980
English is not my first language, so I'd be very grateful for suggestions on how to improve the wording, etc.
/cc @julienrf @odersky @sjrd
2 Likes
I just checked and if I write class Foo private (x: Int)
, then I do get a private constructor in Scala 3.
However, this isn’t the case in Scala 2, I think the best we could do there is to emit the constructor as ACC_SYNTHETIC
to make it invisible to javac without affecting binary compatibility.
Even if you have both class and companion object?
case class Foo private (x: Int)
object Foo:
def apply(x: Int): Foo = new Foo(x)
Ah yeah, in that case we’re back to the public constructor which could be hidden with ACC_SYNTHETIC (we could do better with java 9+ nestmates but that’s more work).
2 Likes
sjrd
January 2, 2023, 12:03am
5
Have we ever considered making these things package private (the real JVM package private, not Scala’s version)?
1 Like
Thank you for the suggestion, that sounds like a good solution. I have created issues for Scala 2 and Scala 3:
opened 05:05PM - 10 Jan 23 UTC
itype:bug
area:backend
Classes with private constructors are actually public at the bytecode level, thi… s creates two issues:
- it can be called from Java code
- [MiMa](https://github.com/lightbend/mima) reports error if its signature changes
## Compiler version
3.2.1
## Minimized code
```Scala
// Foo.scala
package bug
class Foo private (x: Int)
object Foo:
def apply(x: Int) = new Foo(x)
```
~~~ java
// Bar.java
package bug
class Bar {
public void bar() {
Foo foo = new Foo(42);
}
}
~~~
## Output
The code compiles.
## Expectation
The Java compilation should fail.
## Possible solution
In a [discussion](https://contributors.scala-lang.org/t/private-primary-constructor-vs-mima/6050/2), @smarter suggested to emit the constructor as `ACC_SYNTHETIC` to make it non-accessible from Java, and ignored by mima (see lightbend/mima#92). Note that that change would still be binary compatible with possible existing Java code that would call such constructors (but it would be source incompatible).
Related Scala 2 issue: https://github.com/scala/bug/issues/12711
opened 05:08PM - 10 Jan 23 UTC
backend
Classes with private constructors are actually public at the bytecode level, thi… s creates two issues:
- it can be called from Java code
- [MiMa](https://github.com/lightbend/mima) reports error if its signature changes
## Reproduction steps
```Scala
// Foo.scala
package bug
class Foo private (x: Int)
object Foo {
def apply(x: Int) = new Foo(x)
}
```
~~~ java
// Bar.java
package bug
class Bar {
public void bar() {
Foo foo = new Foo(42);
}
}
~~~
## Output
The code compiles.
## Expectation
The Java compilation should fail.
## Possible solution
In a [discussion](https://contributors.scala-lang.org/t/private-primary-constructor-vs-mima/6050/2), @smarter suggested to emit the constructor as `ACC_SYNTHETIC` to make it non-accessible from Java, and ignored by mima (see lightbend/mima#92). Note that that change would still be binary compatible with possible existing Java code that would call such constructors (but it would be source incompatible).
Related Scala 3 issue: https://github.com/lampepfl/dotty/issues/16651
1 Like
After more discussion, we concluded that this should be handled at the MiMa level, so I have created that issue:
opened 08:17AM - 13 Jan 23 UTC
MiMa flags changes in private constructor signatures:
~~~ scala
// v1.scala
…
case class Person private (name: String)
object Person:
def apply(name: String): Person = new Person(name)
~~~
~~~ scala
// v2.scala
case class Person private (name: String, address: Option[String])
object Person:
def apply(name: String): Person = new Person(name, None)
def apply(name: String, address: String): Person = new Person(name, Some(address))
~~~
Here, MiMa fails with a message like:
> method this(java.lang.String)Unit in class Person does not have a correspondent in current version
However, the constructor is private, so the change is safe.
What happens is that although the constructor can’t be called from Scala code, it is effectively public in the bytecode (just like `private[x]` members).
MiMa should detect this pattern and ignore the changes to private class constructors.
Related discussions:
https://github.com/scala/bug/issues/12711
https://github.com/lampepfl/dotty/issues/16651
https://github.com/scala/docs.scala-lang/pull/2662
https://contributors.scala-lang.org/t/private-primary-constructor-vs-mima/6050
PR ignoring `private[x]` members: https://github.com/lightbend/mima/pull/583
1 Like