Path-dependent types vs. "underlying types", which ones are checked?

989 views Asked by At

When using path-dependent types with reflection I am getting a type mismatch error even though I have matching "underlying types". What are these "non-underlying types" and why are they checked instead of the "underlying types"?

In the code below, I want the compare method to accept only same-type subclasses of A as arguments. The error is on the last line.

abstract class A(val a:Int) {
  type Impl <: A
  def compare(other:Impl) {
    if(a==other.a) println("equal") else println("diff")
  }
}
class B(a:Int) extends A(a) {type Impl = B}

object Test {
  def newInst(a: Int, className: String) = {
    val constr = Class.forName(className).getConstructors()(0)
    constr.newInstance(a.asInstanceOf[AnyRef]).asInstanceOf[A]
  }

  def main(args: Array[String]) {
    val b1 = newInst(4, "B")
    val b2 = newInst(5, "B")
    b1.compare(b2)   // type mismatch error here
  }
}

On the last line I get this error:

error: type mismatch;
found   : b2.type (with underlying type A)
required: b1.Impl

Since the type of b2 is the same as the type of b1 (which is A), I expected this to not generate an error. For some reason these path-dependent types are different than the "underlying types" when using reflection. Why?

If I do not use reflection, it works:

val b1 = new B(4)
val b2 = new B(5)
b1.compare(b2)  // no errors

(I do need to use reflection in my case). Can newInst() return the object as class "B", using reflection? Would this help? Is there type erasure when using abstract types?

This is the only reference I found (on this forum) about the same error, but it may not be related.

2

There are 2 answers

1
Alexey Romanov On BEST ANSWER

This doesn't have anything to do with reflection. The type of b1 and b2 is A (since that's the return type of newInst). For call b1.compare(b2) to compile, b2 must have type b1.Impl. The compiler knows only that it is some subtype of A, but it doesn't know which one. Since you can't pass an A where some subtype of A is required, you get an error.

In the example

val b1 = new B(4)
val b2 = new B(5)
b1.compare(b2)  // no errors

both variables have type B, and B#Impl is B, so everything typechecks.

5
psp On

I just figured that out too. The expression "underlying type" is a bit misleading, while it actually means "parent type". Ideally I would like to cast the return type of newInst to the class of "className" but that looks like it is not possible in Scala. "

It does not mean "parent type". That type does happen to be a parent. An underlying type refers to the widening of a singleton type. The type of the number 5 is a singleton; its underlying type is Int. Most people would not call Int the "parent type". (Even though, again, it is.)

As to the question, you can just cast it. Nothing will stand in your way: scala won't and java can't.

def main(args: Array[String]) {
  val b1 = newInst(4, "B")
  val b2 = newInst(5, "B")
  b1.compare(b2.asInstanceOf[b1.Impl])
}
% scala Test
diff