Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type inference on class member widened to abstract definition #17324

Closed
ftucky opened this issue Apr 21, 2023 · 4 comments
Closed

Type inference on class member widened to abstract definition #17324

ftucky opened this issue Apr 21, 2023 · 4 comments

Comments

@ftucky
Copy link

ftucky commented Apr 21, 2023

Compiler version

3.2.2 , 3.3.0-RC3

Minimized code

class A
class B extends A:
  def m = true

//transparent inline def choose(b: Boolean): A =
//  if b then new A else new B

def choose(b:Boolean):B = new B

class X:
  val obj1 : A
  val obj2 : A
  val obj3 : A

class Y extends X:
  val obj0     = choose(false) // type B ( from inference   )
  val obj1     = choose(false) // type A ( from X.this.obj1 )
  val obj2 : B = choose(false) // type B ( from ascription  )
  final val obj3 = choose(false) // type B ( from inference )
  

val y = new Y
y.obj0.m // compiles          EXPECTED
y.obj1.m // compile error   UNEXPECTED
y.obj2.m // compiles          EXPECTED
y.obj3.m // compiles          EXPECTED // Why different from obj1 case ?

Output

y.obj1.m ---- m is not a member of A

Expectation

The issue is the type of the val objN in Y.

  • obj0: EXPECTED. Type is B as inferred from choose( ) return type.
  • obj1: UNEXPECTED. Type is A, widened to the abstract definition val obj1:A in X
  • obj2: EXPECTED. Type is B, from the type-ascription.
  • obj3: ???. Type is B. The final modifier has an impact on the member type (vs. obj1), and seems to restore expected type inference. On the other hand making the entire class Y final has no impact.

Observations

You may notice a pattern very close to the one described here. The bug is not caused bug transparent inline, but loses the on-purpose crafted type.

In scala2 the type was the expected one. 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.
  • Is the change of behavior between scala2 and scala3 intentional ?
  • Is the impact of final on the type of the member expected ?
@ftucky ftucky added itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label labels Apr 21, 2023
@jchyb jchyb added area:typer and removed stat:needs triage Every issue needs to have an "area" and "itype" label labels Apr 21, 2023
@som-snytt
Copy link
Contributor

This is expected, because the override is inferred to have the type of the member that is overridden. This is true for scala 2 under -Xsource:3.

The final val case is different because... no, I can't explain that one.

@ftucky
Copy link
Author

ftucky commented Apr 23, 2023

The change from scala2 to scala3 has been taken, to prevent unwanted exposure of implementation detail (of specific type information in the subclass). Correct ?

Sometimes, the sub-class is not simply an implementation but a specialization of its super-class.
Scala provides mechanisms which are clearly designed to pass information in the returned type, which are defeated by the new behavior.

I already mentioned transparent inline defs.
Another one is local selectable instances.

The code below is the illustration of the feature in scala-doc:

trait Vehicle extends reflect.Selectable:
  val wheels: Int

val i3 = new Vehicle: // i3: Vehicle { val range: Int }
  val wheels = 4
  val range = 240

i3.range

The feature is designed to craft the correct refined type Vehicle {val range:Int}.
Now, if I translate the very same example in the context of a trait Fleet:

trait Vehicle extends reflect.Selectable:
  val wheels: Int

trait Fleet:
  val vehicle : Vehicle

class MyFleet extends Fleet :
  val vehicle = new Vehicle:  // vehicle : Vehicle ** UNREFINED ** 
    val wheels = 4
    val range = 240

val f = new MyFleet
f.vehicle.range  // Does not compile 

The precious type information is lost !

Today, I may use final modifier to restore the desired behavior. I do not know whether this is specific final effect has been designed on purpose as an escape hatch.

@ftucky
Copy link
Author

ftucky commented Apr 25, 2023

Some pointers : (Thank you @smarter !)

why a class member should be widened to its super interface type:

A SIP, for a precise modifier:

Why are final val narrowed to their precise type and on their way to deprecation: (use inline val instead)

Closing.

@ftucky ftucky closed this as completed Apr 25, 2023
@som-snytt
Copy link
Contributor

I would expect final val to be magical only if the RHS is a "constant value expression".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants