tl;dr: I expect the following code to fail to compile as NotABlob
fails the Blob
type constraint:
open class Blob
interface NotABlob
fun <T: Blob> foo(): T {
TODO()
}
val notABlob: NotABlob = foo()
However, it successfully compiles and runs, throwing the expected exception when TODO()
is reached.
Exception in thread "main" kotlin.NotImplementedError: An operation is not implemented.
Why? I don't yet know
If I define a function with a generic type constraint, I expect that it shouldn't be possible to call that function with a type that violates that constraint. Example:
open class Blob
fun <T: Blob> foo(): T {
TODO()
}
I should only be able to call foo()
when the return type is a Blob
or a subtype of Blob
.
val blob: Blob = foo()
As expected, this compiles and results in the expected runtime exception from TODO()
:
Exception in thread "main" kotlin.NotImplementedError: An operation is not implemented.
If I attempt to call it with a class that's not a subtype of Blob
, as expected it fails to compile, but the failure reason was unexpected:
open class NotABlob
val notABlob: NotABlob = foo()
error: type mismatch: inferred type is Blob but NotABlob was expected
val notABlob: NotABlob = foo()
^
My assumptions about how type inference works needs adjustment. I would have expected the above code to be equivalent to this:
open class NotABlob
val notABlob: NotABlob = foo<NotABlob>()
And for it to fail to compile with the same error this new snippet generates:
error: type argument is not within its bounds: should be subtype of 'Blob'
val notABlob: NotABlob = foo<NotABlob>()
^
So I don't get the expected error, but at least I'm not allowed to violate the type constraint.
But here's where it gets interesting. If I call foo()
where the return type would be inferred to be an interface type that has nothing to do with Blob
, surprisingly it compiles and we get the TODO()
runtime exception!
interface NotABlob
val notABlob: NotABlob = foo()
Exception in thread "main" kotlin.NotImplementedError: An operation is not implemented.
I find this very surprising. What type is the compiler inferring T
to be that meets the required type constraint? Let's change foo()
so we can get access to that type information:
inline fun <reified T: Blob> foo(): T {
println("T is a '${T::class}")
TODO()
}
Before hitting the TODO()
exception, we see:
T is a 'class kotlin.Any'
Since it passed the generic type constraint, it would seem that the compiler must think that Any
is a subtype of Blob
, which can't possibly be true.
println("Is Any a subclass of Blob? ${Any::class.isSubclassOf(Blob::class)}")
Is Any a subclass of Blob? false
Correct! So how did this get past the generic type constraint? It's not just because the type is an interface. If we explicitly specify the type in the call to foo, it fails to compile as expected:
val notABlob: NotABlob = foo<NotABlob>()
error: type argument is not within its bounds: should be subtype of 'Blob'
val notABlob: NotABlob = foo<NotABlob>()
^
I'd love to learn from anyone with a deeper working knowledge of type inference and generic type constraints who can explain this to me. Please comment here or at @[email protected].