Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save humblehacker/f965eeceb1b718d70f8d4a162ba8ba27 to your computer and use it in GitHub Desktop.
Save humblehacker/f965eeceb1b718d70f8d4a162ba8ba27 to your computer and use it in GitHub Desktop.
Kotlin generic type constraints are ignored when the inferred type is an interface.md

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 @humblehacker@mastodon.online.

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