Scala 2 has several "back door rules" that were never documented or explained, but they are used in many open-sourced projects and even becomes their own patterns, one of such rules is the fact that case class's companion object has the same type signature of a unary or nullary function, and can be used in overriding properties of the super-types of its outer type:
{
// unary function (without eta-expansion) overriden by unary case class constructor
trait A {
type CC
def CC: Int => CC // only works in Scala 2
}
object AA extends A {
case class CC(v: Int) {}
}
}
{
// nullary function (without eta-expansion) overriden by nullary case class constructor
trait A {
type CC
def CC: () => CC // only works in Scala 2
}
object AA extends A {
case class CC() {}
}
}
In Scala 3, this rule was revoked, and no substitutes were proposed (tested with Scala 3.3 nightly):
[Error] /home/peng/git/dottyspike/src/main/scala/com/tribbloids/spike/dotty/CaseClassOverridingRule.scala:54:18: error overriding method CC in trait A of type => Int => com.tribbloids.spike.dotty.CaseClassOverridingRule.AA.CC;
object CC has incompatible type
Explanation
===========
I tried to show that
object com.tribbloids.spike.dotty.CaseClassOverridingRule.AA.CC
conforms to
=> Int => com.tribbloids.spike.dotty.CaseClassOverridingRule.AA.CC
but none of the attempts shown below succeeded:
==> object com.tribbloids.spike.dotty.CaseClassOverridingRule.AA.CC <: => Int => com.tribbloids.spike.dotty.CaseClassOverridingRule.AA.CC class dotty.tools.dotc.core.Types$CachedTypeRef class dotty.tools.dotc.core.Types$CachedExprType = false
The tests were made under the empty constraint
[Error] /home/peng/git/dottyspike/src/main/scala/com/tribbloids/spike/dotty/CaseClassOverridingRule.scala:69:18: error overriding method CC in trait A of type => () => com.tribbloids.spike.dotty.CaseClassOverridingRule.AA.CC;
object CC has incompatible type
Explanation
===========
I tried to show that
object com.tribbloids.spike.dotty.CaseClassOverridingRule.AA.CC
conforms to
=> () => com.tribbloids.spike.dotty.CaseClassOverridingRule.AA.CC
but none of the attempts shown below succeeded:
==> object com.tribbloids.spike.dotty.CaseClassOverridingRule.AA.CC <: => () => com.tribbloids.spike.dotty.CaseClassOverridingRule.AA.CC class dotty.tools.dotc.core.Types$CachedTypeRef class dotty.tools.dotc.core.Types$CachedExprType = false
Most migration tools for Scala 3 won't rewrite it. As I result, I have to revise the above example manually, and here is the only working case:
{
// unary function (without eta-expansion) overriden by unary case class constructor
trait A {
type CC
def CC: { def apply(v: Int): CC }
}
object AA extends A {
case class CC(v: Int) {}
}
}
{
// nullary function (without eta-expansion) overriden by nullary case class constructor
trait A {
type CC
def CC: { def apply(): CC }
}
object AA extends A {
case class CC() {}
}
}
This is not an ideal substitute, defining CC in structural refined types causes JVM to verify it with slow reflection in runtime, the verification is also incomplete due to type erasure. In addition, refined types are not fully understood in DOT calculus and Scala compiler may change its behaviour in later versions.
If I need to migrate a library that uses the above rule, what type signature should I use?
Would you accept a typeclass-based solution?
We can define your desired trait, not as a traditional Java-style interface, but as a typeclass that takes a type parameter
Tand requires theCCtype and constructor.Now we just write our singleton
AAwith no inheritance.We could manually implement the typeclass at this point.
But that sounds like boilerplate, and if there's one thing Scala devs hate, it's boilerplate. So instead, let's try this.
That's a mouthful. Let's break that down. We're writing a generic implementation of
A[T]for anyTthat satisfies some basic constraints. Those constraints correspond to our contextual arguments, and they are:sub:Tmust be a subtype of{ type CC }, where we give the nameAuxto the inner typeCCso we can refer to it here. That is,Tmust have a type calledCCdefined on it. Note that structural types that are only used for type-level resolution such as this one do not have any overhead, unlike the ones you propose, which do incur a runtime reflection penalty as you noted.mirror: The inner typeAux(orCCas it was originally called) must be a product type. That's either a tuple or a case class, as far as I know.paramsSub: The constituents inside of that product type must be a single integer. That is,Auxmust contain exactly one integer.Once we have those constraints, we implement
A[T]. We definetype CCto beAux, our auxiliary type. Then we write our constructor function. Themirrorobject I summoned a minute ago gives us the ability to construct our instance from a product, so if we have a correctly-typed tuple, we can make our instance.Example usage:
Scala playground link