Consider the following code : scastie
class A { }
class B extends A { def m = true }
trait X { def obj : A }
class Y extends X { def obj = new B }
val y = new Y
y.obj.m // Compiles in scala2, does not compile in scala3.
What is the type of y.obj
?
It appears that in scala2 y.obj
is a B
, while in scala3 y.obj
is a A
!
As a result looking up for y.obj.m
compiles in scala2, but not in scala3.
Apparently, in scala3, the type of the abstract member def obj: A
in X
takes precedence over the narrower type inference in Y
.
As expected, y.obj
is a B
when :
class Y /* extends X */ { def obj = new B } // member is not previously declared
class Y extends X { def obj : B = new B } // type is forced by ascription
This looks benign, until one uses transparent inline methods to ship back some type information out of a macro.
Here is a more descriptive snippet: scastie
class A
class B extends A:
def m = true
transparent inline def choose(b: Boolean): A =
if b then new A else new B
class X:
def obj1 : A
def obj2 : A
def obj3 : A
class Y extends X:
def obj1 = choose(true) // type A
def obj2 = choose(false) // type A ( from X.this.obj2 )
def obj3 : B = choose(false) // type B ( from ascription )
def obj4 = choose(false) // type B ( from inference )
val y = new Y
y.obj1.m // compile error EXPECTED
y.obj2.m // compile error UNEXPECTED
y.obj3.m // compiles EXPECTED
y.obj4.m // compiles EXPECTED
Is the discrepancy between scala2 and scala3 expected ?
Yes. Here is the intention.
Is there a workaround ?
final
prevents the widening.final val
is expected to be phased out from scala3, superseded by inline val
.inline def
compiles correctly, but also inlines the definition-rhs at the call site, which may not be the intention.inline val
is restricted to literal constants.A SIP about a precise
modifier is in discussion here.