Compile time checks on Generic types that differ between this.type and other types

I’ve come accros some behaviour of the Scala compiler I have an explanation for of which I’m not certain I’m correct. I’d like to get the propper insight for the behaviour and an explanation, and possible example of unwished behaviour, for the current choices done in the compiler.

The following code compiles and runs like a charm, as expected:

trait T1[A]
    {
        type ReturnType = this.type
        val returnValue : this.type = this
        def apply(_x : A) : ReturnType
    }
trait T2[A]
    extends T1[Option[A]]
        {
            def apply(_x : A) : ReturnType
        }
class C1[A]
    extends T1[A]
        {
            def apply(_x : A) : ReturnType = 
                {
                    // some functionality
                    returnValue
                }
        }
class C2[A]
    extends C1[Option[A]]
    with T2[A]
        {
            def apply(_x : A) : ReturnType = apply(Some(_x)) 
        }

However, changing the type and returnValue lines in e.g.:

type ReturnType = String
val returnValue : String = ""

will result in the following message:

<pastie>:23: error: name clash between defined and inherited member:
def apply(_x: Option[A]): T2.this.ReturnType in trait T1 and
def apply(_x: A): T2.this.ReturnType at line 23
have same type after erasure: (_x: Object)String
                def apply(_x : A) : ReturnType
                    ^
<pastie>:38: error: name clash between defined and inherited member:
def apply(_x: Option[A]): C2.this.ReturnType in class C1 and
def apply(_x: A): C2.this.ReturnType at line 38
have same type after erasure: (_x: Object)String
                def apply(_x : A) : ReturnType = apply(Some(_x))
                    ^

My explanation is that type checks for this.type are postponed as at the declaration the exact type for this.type is not known. Only the super-type is known.

The two questions I have are:

  1. Is my assumption correct?

  2. Being as it may that the first code works like a charm, I suspect that the second code would also perform like a charm, but the compiler is too eagerly to do the type check. Why couldn’t it be made that this check is also postponed. My solution now is to duplicate code for my second class where I no longer extends from the first class, but copy the code for the first class and add the extra line.

It’s not really a problem about type checking. It’s more a limitation of the JVM (or a refusal of scalac to work around that limitation…). When ReturnType is String the erased type (the type the JVM sees at runtime) of your apply methods in T2 are both (Object)String and it’s not possible to have 2 methods with the same erased type.
When ReturnType is this.type the erasure of both methods is (Object)T1 and (Object)T2.

Trait T2 has two methods with distinct signatures. In the first case:

def apply(_x : Option[A]) : (Object)T2
def apply(_x : A) : (Object)T2

Note that T2 extends from T1 with generic type of Option[A].
I would suspect that changing the ReturnType to String would result in the following legal construction:

def apply(_x : Option[A]) : String
def apply(_x : A) : String

It is legal if declared in C2, but somehow generates an error if inherited through C1. I still think that Scalac is too strict in its typechecking in this specific case.

No, from the point of view of the JVM, T1 is an interface with a method that takes an Object and returns a String. The JVM doesn’t know anything about generics or type parameters. And T2 inherits that same method, there’s no way around that because T2 extends T1, and then defines another one with the same name and the same erasure.
It’s sort of true that scalac is too strict but it does that because the JVM platform demands it and because the designers of Scala wanted good interoperability between Scala and Java.

1 Like

@Jasper-M Thanks for the clear explanation. I now see where I went wrong with my logic. Luckily I use my own precompiler so that code duplication can be avoided in the given case.